2021-05-12 09:13:25 +02:00
|
|
|
import type * as SimplePeerNamespace from "simple-peer";
|
2021-06-25 18:14:40 +02:00
|
|
|
import { mediaManager } from "./MediaManager";
|
|
|
|
import type { RoomConnection } from "../Connexion/RoomConnection";
|
|
|
|
import { blackListManager } from "./BlackListManager";
|
|
|
|
import type { Subscription } from "rxjs";
|
|
|
|
import type { UserSimplePeerInterface } from "./SimplePeer";
|
2021-07-07 18:07:58 +02:00
|
|
|
import { get, readable, Readable, Unsubscriber } from "svelte/store";
|
2021-08-10 22:35:26 +02:00
|
|
|
import { obtainedMediaConstraintIsMobileStore, obtainedMediaConstraintStore } from "../Stores/MediaStore";
|
2021-07-07 11:24:51 +02:00
|
|
|
import { playersStore } from "../Stores/PlayersStore";
|
2021-07-07 18:07:58 +02:00
|
|
|
import { chatMessagesStore, chatVisibilityStore, newChatMessageStore } from "../Stores/ChatStore";
|
2021-07-15 11:26:04 +02:00
|
|
|
import { getIceServersConfig } from "../Components/Video/utils";
|
2021-08-10 22:35:26 +02:00
|
|
|
import { isMobile } from "../Enum/EnvironmentVariable";
|
2021-06-25 18:14:40 +02:00
|
|
|
|
|
|
|
const Peer: SimplePeerNamespace.SimplePeer = require("simple-peer");
|
2020-08-20 16:56:10 +02:00
|
|
|
|
2021-06-24 10:34:36 +02:00
|
|
|
export type PeerStatus = "connecting" | "connected" | "error" | "closed";
|
|
|
|
|
2021-06-25 18:14:40 +02:00
|
|
|
export const MESSAGE_TYPE_CONSTRAINT = "constraint";
|
|
|
|
export const MESSAGE_TYPE_MESSAGE = "message";
|
|
|
|
export const MESSAGE_TYPE_BLOCKED = "blocked";
|
|
|
|
export const MESSAGE_TYPE_UNBLOCKED = "unblocked";
|
2020-08-20 16:56:10 +02:00
|
|
|
/**
|
|
|
|
* A peer connection used to transmit video / audio signals between 2 peers.
|
|
|
|
*/
|
|
|
|
export class VideoPeer extends Peer {
|
2020-10-20 18:02:44 +02:00
|
|
|
public toClose: boolean = false;
|
|
|
|
public _connected: boolean = false;
|
2021-02-02 18:19:51 +01:00
|
|
|
private remoteStream!: MediaStream;
|
|
|
|
private blocked: boolean = false;
|
2021-06-11 11:29:36 +02:00
|
|
|
public readonly userId: number;
|
2021-07-07 11:24:51 +02:00
|
|
|
public readonly userUuid: string;
|
2021-06-11 11:29:36 +02:00
|
|
|
public readonly uniqueId: string;
|
2021-02-02 18:19:51 +01:00
|
|
|
private onBlockSubscribe: Subscription;
|
|
|
|
private onUnBlockSubscribe: Subscription;
|
2021-06-11 11:29:36 +02:00
|
|
|
public readonly streamStore: Readable<MediaStream | null>;
|
2021-06-24 10:34:36 +02:00
|
|
|
public readonly statusStore: Readable<PeerStatus>;
|
2021-06-25 18:14:40 +02:00
|
|
|
public readonly constraintsStore: Readable<MediaStreamConstraints | null>;
|
2021-07-07 18:07:58 +02:00
|
|
|
private newMessageunsubscriber: Unsubscriber | null = null;
|
2021-07-15 16:31:43 +02:00
|
|
|
private closing: Boolean = false; //this is used to prevent destroy() from being called twice
|
2021-02-02 18:19:51 +01:00
|
|
|
|
2021-06-25 18:14:40 +02:00
|
|
|
constructor(
|
|
|
|
public user: UserSimplePeerInterface,
|
|
|
|
initiator: boolean,
|
|
|
|
public readonly userName: string,
|
|
|
|
private connection: RoomConnection,
|
|
|
|
localStream: MediaStream | null
|
|
|
|
) {
|
2020-08-20 16:56:10 +02:00
|
|
|
super({
|
2021-07-15 11:26:04 +02:00
|
|
|
initiator,
|
2020-08-20 16:56:10 +02:00
|
|
|
config: {
|
2021-07-15 11:26:04 +02:00
|
|
|
iceServers: getIceServersConfig(user),
|
2021-06-25 18:14:40 +02:00
|
|
|
},
|
2020-08-20 16:56:10 +02:00
|
|
|
});
|
2021-02-16 18:13:30 +01:00
|
|
|
|
2021-02-02 18:19:51 +01:00
|
|
|
this.userId = user.userId;
|
2021-07-07 11:24:51 +02:00
|
|
|
this.userUuid = playersStore.getPlayerById(this.userId)?.userUuid || "";
|
2021-06-25 18:14:40 +02:00
|
|
|
this.uniqueId = "video_" + this.userId;
|
2021-06-11 11:29:36 +02:00
|
|
|
|
2021-06-25 18:14:40 +02:00
|
|
|
this.streamStore = readable<MediaStream | null>(null, (set) => {
|
|
|
|
const onStream = (stream: MediaStream | null) => {
|
2021-06-11 11:29:36 +02:00
|
|
|
set(stream);
|
|
|
|
};
|
|
|
|
const onData = (chunk: Buffer) => {
|
2021-06-25 18:14:40 +02:00
|
|
|
this.on("data", (chunk: Buffer) => {
|
|
|
|
const message = JSON.parse(chunk.toString("utf8"));
|
2021-06-11 11:29:36 +02:00
|
|
|
if (message.type === MESSAGE_TYPE_CONSTRAINT) {
|
|
|
|
if (!message.video) {
|
|
|
|
set(null);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
2021-06-25 18:14:40 +02:00
|
|
|
};
|
2021-06-11 11:29:36 +02:00
|
|
|
|
2021-06-25 18:14:40 +02:00
|
|
|
this.on("stream", onStream);
|
|
|
|
this.on("data", onData);
|
2021-06-11 11:29:36 +02:00
|
|
|
|
|
|
|
return () => {
|
2021-06-25 18:14:40 +02:00
|
|
|
this.off("stream", onStream);
|
|
|
|
this.off("data", onData);
|
2021-06-11 11:29:36 +02:00
|
|
|
};
|
|
|
|
});
|
|
|
|
|
2021-06-25 18:14:40 +02:00
|
|
|
this.constraintsStore = readable<MediaStreamConstraints | null>(null, (set) => {
|
2021-06-11 11:29:36 +02:00
|
|
|
const onData = (chunk: Buffer) => {
|
2021-06-25 18:14:40 +02:00
|
|
|
const message = JSON.parse(chunk.toString("utf8"));
|
|
|
|
if (message.type === MESSAGE_TYPE_CONSTRAINT) {
|
2021-06-11 11:29:36 +02:00
|
|
|
set(message);
|
|
|
|
}
|
2021-06-25 18:14:40 +02:00
|
|
|
};
|
2021-06-11 11:29:36 +02:00
|
|
|
|
2021-06-25 18:14:40 +02:00
|
|
|
this.on("data", onData);
|
2021-06-11 11:29:36 +02:00
|
|
|
|
|
|
|
return () => {
|
2021-06-25 18:14:40 +02:00
|
|
|
this.off("data", onData);
|
2021-06-11 11:29:36 +02:00
|
|
|
};
|
|
|
|
});
|
|
|
|
|
2021-06-24 10:34:36 +02:00
|
|
|
this.statusStore = readable<PeerStatus>("connecting", (set) => {
|
2021-06-11 11:29:36 +02:00
|
|
|
const onConnect = () => {
|
2021-06-25 18:14:40 +02:00
|
|
|
set("connected");
|
2021-06-11 11:29:36 +02:00
|
|
|
};
|
|
|
|
const onError = () => {
|
2021-06-25 18:14:40 +02:00
|
|
|
set("error");
|
2021-06-11 11:29:36 +02:00
|
|
|
};
|
|
|
|
const onClose = () => {
|
2021-06-25 18:14:40 +02:00
|
|
|
set("closed");
|
2021-06-11 11:29:36 +02:00
|
|
|
};
|
|
|
|
|
2021-06-25 18:14:40 +02:00
|
|
|
this.on("connect", onConnect);
|
|
|
|
this.on("error", onError);
|
|
|
|
this.on("close", onClose);
|
2021-06-11 11:29:36 +02:00
|
|
|
|
|
|
|
return () => {
|
2021-06-25 18:14:40 +02:00
|
|
|
this.off("connect", onConnect);
|
|
|
|
this.off("error", onError);
|
|
|
|
this.off("close", onClose);
|
2021-06-11 11:29:36 +02:00
|
|
|
};
|
|
|
|
});
|
2020-08-28 16:29:21 +02:00
|
|
|
|
2020-08-20 16:56:10 +02:00
|
|
|
//start listen signal for the peer connection
|
2021-06-25 18:14:40 +02:00
|
|
|
this.on("signal", (data: unknown) => {
|
2020-08-20 16:56:10 +02:00
|
|
|
this.sendWebrtcSignal(data);
|
|
|
|
});
|
|
|
|
|
2021-06-25 18:14:40 +02:00
|
|
|
this.on("stream", (stream: MediaStream) => this.stream(stream));
|
2020-08-20 16:56:10 +02:00
|
|
|
|
2021-06-25 18:14:40 +02:00
|
|
|
this.on("close", () => {
|
2020-10-20 18:02:44 +02:00
|
|
|
this._connected = false;
|
|
|
|
this.toClose = true;
|
2020-08-20 22:23:22 +02:00
|
|
|
this.destroy();
|
2020-08-20 16:56:10 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
2021-06-25 18:14:40 +02:00
|
|
|
this.on("error", (err: any) => {
|
2020-08-20 16:56:10 +02:00
|
|
|
console.error(`error => ${this.userId} => ${err.code}`, err);
|
2021-02-02 18:19:51 +01:00
|
|
|
mediaManager.isError("" + this.userId);
|
2020-08-20 16:56:10 +02:00
|
|
|
});
|
|
|
|
|
2021-06-25 18:14:40 +02:00
|
|
|
this.on("connect", () => {
|
2020-10-20 18:02:44 +02:00
|
|
|
this._connected = true;
|
2021-07-07 18:07:58 +02:00
|
|
|
chatMessagesStore.addIncomingUser(this.userId);
|
|
|
|
|
|
|
|
this.newMessageunsubscriber = newChatMessageStore.subscribe((newMessage) => {
|
|
|
|
if (!newMessage) return;
|
|
|
|
this.write(
|
|
|
|
new Buffer(
|
|
|
|
JSON.stringify({
|
|
|
|
type: MESSAGE_TYPE_MESSAGE,
|
|
|
|
message: newMessage,
|
|
|
|
})
|
|
|
|
)
|
|
|
|
); //send more data
|
|
|
|
newChatMessageStore.set(null); //This is to prevent a newly created SimplePeer to send an old message a 2nd time. Is there a better way?
|
|
|
|
});
|
2020-08-20 16:56:10 +02:00
|
|
|
});
|
|
|
|
|
2021-06-25 18:14:40 +02:00
|
|
|
this.on("data", (chunk: Buffer) => {
|
|
|
|
const message = JSON.parse(chunk.toString("utf8"));
|
|
|
|
if (message.type === MESSAGE_TYPE_CONSTRAINT) {
|
2020-11-27 14:51:50 +01:00
|
|
|
if (message.audio) {
|
2020-10-25 21:59:14 +01:00
|
|
|
mediaManager.enabledMicrophoneByUserId(this.userId);
|
|
|
|
} else {
|
|
|
|
mediaManager.disabledMicrophoneByUserId(this.userId);
|
|
|
|
}
|
|
|
|
|
2020-11-27 14:51:50 +01:00
|
|
|
if (message.video || message.screen) {
|
2020-10-25 21:59:14 +01:00
|
|
|
mediaManager.enabledVideoByUserId(this.userId);
|
|
|
|
} else {
|
|
|
|
mediaManager.disabledVideoByUserId(this.userId);
|
|
|
|
}
|
2021-08-10 22:35:26 +02:00
|
|
|
if (message.isMobile != undefined) {
|
|
|
|
obtainedMediaConstraintIsMobileStore.set(message.isMobile);
|
|
|
|
}
|
2021-06-25 18:14:40 +02:00
|
|
|
} else if (message.type === MESSAGE_TYPE_MESSAGE) {
|
2021-07-07 18:07:58 +02:00
|
|
|
if (!blackListManager.isBlackListed(this.userUuid)) {
|
|
|
|
chatMessagesStore.addExternalMessage(this.userId, message.message);
|
2021-02-02 18:19:51 +01:00
|
|
|
}
|
2021-06-25 18:14:40 +02:00
|
|
|
} else if (message.type === MESSAGE_TYPE_BLOCKED) {
|
2021-02-16 09:58:08 +01:00
|
|
|
//FIXME when A blacklists B, the output stream from A is muted in B's js client. This is insecure since B can manipulate the code to unmute A stream.
|
2021-02-02 18:19:51 +01:00
|
|
|
// Find a way to block A's output stream in A's js client
|
|
|
|
//However, the output stream stream B is correctly blocked in A client
|
|
|
|
this.blocked = true;
|
|
|
|
this.toggleRemoteStream(false);
|
2021-06-25 18:14:40 +02:00
|
|
|
} else if (message.type === MESSAGE_TYPE_UNBLOCKED) {
|
2021-02-02 18:19:51 +01:00
|
|
|
this.blocked = false;
|
|
|
|
this.toggleRemoteStream(true);
|
2020-08-20 16:56:10 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2021-06-25 18:14:40 +02:00
|
|
|
this.once("finish", () => {
|
2020-10-20 18:02:44 +02:00
|
|
|
this._onFinish();
|
|
|
|
});
|
|
|
|
|
2021-05-29 22:34:38 +02:00
|
|
|
this.pushVideoToRemoteUser(localStream);
|
2021-07-07 11:24:51 +02:00
|
|
|
this.onBlockSubscribe = blackListManager.onBlockStream.subscribe((userUuid) => {
|
|
|
|
if (userUuid === this.userUuid) {
|
2021-02-02 18:19:51 +01:00
|
|
|
this.toggleRemoteStream(false);
|
|
|
|
this.sendBlockMessage(true);
|
|
|
|
}
|
|
|
|
});
|
2021-07-07 11:24:51 +02:00
|
|
|
this.onUnBlockSubscribe = blackListManager.onUnBlockStream.subscribe((userUuid) => {
|
|
|
|
if (userUuid === this.userUuid) {
|
2021-02-02 18:19:51 +01:00
|
|
|
this.toggleRemoteStream(true);
|
|
|
|
this.sendBlockMessage(false);
|
|
|
|
}
|
|
|
|
});
|
2021-02-16 09:58:08 +01:00
|
|
|
|
2021-07-07 11:24:51 +02:00
|
|
|
if (blackListManager.isBlackListed(this.userUuid)) {
|
2021-06-25 18:14:40 +02:00
|
|
|
this.sendBlockMessage(true);
|
2021-02-02 18:19:51 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private sendBlockMessage(blocking: boolean) {
|
2021-06-25 18:14:40 +02:00
|
|
|
this.write(
|
|
|
|
new Buffer(
|
|
|
|
JSON.stringify({
|
|
|
|
type: blocking ? MESSAGE_TYPE_BLOCKED : MESSAGE_TYPE_UNBLOCKED,
|
|
|
|
name: this.userName.toUpperCase(),
|
|
|
|
userId: this.userId,
|
|
|
|
message: "",
|
|
|
|
})
|
|
|
|
)
|
|
|
|
);
|
2021-02-02 18:19:51 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private toggleRemoteStream(enable: boolean) {
|
2021-06-25 18:14:40 +02:00
|
|
|
this.remoteStream.getTracks().forEach((track) => (track.enabled = enable));
|
2021-02-02 18:19:51 +01:00
|
|
|
mediaManager.toggleBlockLogo(this.userId, !enable);
|
2020-08-20 16:56:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
private sendWebrtcSignal(data: unknown) {
|
|
|
|
try {
|
|
|
|
this.connection.sendWebrtcSignal(data, this.userId);
|
2021-06-25 18:14:40 +02:00
|
|
|
} catch (e) {
|
2020-08-20 16:56:10 +02:00
|
|
|
console.error(`sendWebrtcSignal => ${this.userId}`, e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sends received stream to screen.
|
|
|
|
*/
|
2020-11-27 14:51:50 +01:00
|
|
|
private stream(stream: MediaStream) {
|
|
|
|
try {
|
2021-02-02 18:19:51 +01:00
|
|
|
this.remoteStream = stream;
|
2021-07-07 11:24:51 +02:00
|
|
|
if (blackListManager.isBlackListed(this.userUuid) || this.blocked) {
|
2021-02-02 18:19:51 +01:00
|
|
|
this.toggleRemoteStream(false);
|
|
|
|
}
|
2021-06-25 18:14:40 +02:00
|
|
|
} catch (err) {
|
2020-11-27 14:51:50 +01:00
|
|
|
console.error(err);
|
2020-08-20 16:56:10 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This is triggered twice. Once by the server, and once by a remote client disconnecting
|
|
|
|
*/
|
2021-07-15 16:31:43 +02:00
|
|
|
public destroy(): void {
|
2020-08-20 16:56:10 +02:00
|
|
|
try {
|
2021-06-25 18:14:40 +02:00
|
|
|
this._connected = false;
|
2021-07-15 16:31:43 +02:00
|
|
|
if (!this.toClose || this.closing) {
|
2020-10-20 18:02:44 +02:00
|
|
|
return;
|
|
|
|
}
|
2021-07-15 16:31:43 +02:00
|
|
|
this.closing = true;
|
2021-02-02 18:19:51 +01:00
|
|
|
this.onBlockSubscribe.unsubscribe();
|
|
|
|
this.onUnBlockSubscribe.unsubscribe();
|
2021-07-07 18:07:58 +02:00
|
|
|
if (this.newMessageunsubscriber) this.newMessageunsubscriber();
|
|
|
|
chatMessagesStore.addOutcomingUser(this.userId);
|
2021-07-15 16:31:43 +02:00
|
|
|
super.destroy();
|
2020-08-20 16:56:10 +02:00
|
|
|
} catch (err) {
|
2021-06-25 18:14:40 +02:00
|
|
|
console.error("VideoPeer::destroy", err);
|
2020-08-20 16:56:10 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-25 18:14:40 +02:00
|
|
|
_onFinish() {
|
|
|
|
if (this.destroyed) return;
|
2020-10-20 18:02:44 +02:00
|
|
|
const destroySoon = () => {
|
|
|
|
this.destroy();
|
2021-06-25 18:14:40 +02:00
|
|
|
};
|
2020-10-20 18:02:44 +02:00
|
|
|
if (this._connected) {
|
|
|
|
destroySoon();
|
|
|
|
} else {
|
2021-06-25 18:14:40 +02:00
|
|
|
this.once("connect", destroySoon);
|
2020-10-20 18:02:44 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-29 22:34:38 +02:00
|
|
|
private pushVideoToRemoteUser(localStream: MediaStream | null) {
|
2020-08-20 16:56:10 +02:00
|
|
|
try {
|
2021-06-25 18:14:40 +02:00
|
|
|
this.write(
|
2021-08-10 22:35:26 +02:00
|
|
|
new Buffer(
|
|
|
|
JSON.stringify({
|
|
|
|
type: MESSAGE_TYPE_CONSTRAINT,
|
|
|
|
...get(obtainedMediaConstraintStore),
|
|
|
|
isMobile: isMobile(),
|
|
|
|
})
|
|
|
|
)
|
2021-06-25 18:14:40 +02:00
|
|
|
);
|
2020-08-20 16:56:10 +02:00
|
|
|
|
2021-06-25 18:14:40 +02:00
|
|
|
if (!localStream) {
|
2020-08-20 16:56:10 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const track of localStream.getTracks()) {
|
|
|
|
this.addTrack(track, localStream);
|
|
|
|
}
|
2021-06-25 18:14:40 +02:00
|
|
|
} catch (e) {
|
2020-08-20 16:56:10 +02:00
|
|
|
console.error(`pushVideoToRemoteUser => ${this.userId}`, e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|