123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744 |
- import { util } from "./util";
- import logger, { LogLevel } from "./logger";
- import { Socket } from "./socket";
- import { MediaConnection } from "./mediaconnection";
- import type { DataConnection } from "./dataconnection/DataConnection";
- import {
- ConnectionType,
- PeerErrorType,
- ServerMessageType,
- SocketEventType,
- } from "./enums";
- import type { ServerMessage } from "./servermessage";
- import { API } from "./api";
- import type {
- CallOption,
- PeerConnectOption,
- PeerJSOption,
- } from "./optionInterfaces";
- import { BinaryPack } from "./dataconnection/BufferedConnection/BinaryPack";
- import { Raw } from "./dataconnection/BufferedConnection/Raw";
- import { Json } from "./dataconnection/BufferedConnection/Json";
- import { EventEmitterWithError, PeerError } from "./peerError";
- class PeerOptions implements PeerJSOption {
- /**
- * Prints log messages depending on the debug level passed in.
- */
- debug?: LogLevel;
- /**
- * Server host. Defaults to `0.peerjs.com`.
- * Also accepts `'/'` to signify relative hostname.
- */
- host?: string;
- /**
- * Server port. Defaults to `443`.
- */
- port?: number;
- /**
- * The path where your self-hosted PeerServer is running. Defaults to `'/'`
- */
- path?: string;
- /**
- * API key for the PeerServer.
- * This is not used anymore.
- * @deprecated
- */
- key?: string;
- token?: string;
- /**
- * Configuration hash passed to RTCPeerConnection.
- * This hash contains any custom ICE/TURN server configuration.
- *
- * Defaults to {@apilink util.defaultConfig}
- */
- config?: any;
- /**
- * Set to true `true` if you're using TLS.
- * :::danger
- * If possible *always use TLS*
- * :::
- */
- secure?: boolean;
- pingInterval?: number;
- referrerPolicy?: ReferrerPolicy;
- logFunction?: (logLevel: LogLevel, ...rest: any[]) => void;
- serializers?: SerializerMapping;
- }
- export { type PeerOptions };
- export interface SerializerMapping {
- [key: string]: new (
- peerId: string,
- provider: Peer,
- options: any,
- ) => DataConnection;
- }
- export interface PeerEvents {
- /**
- * Emitted when a connection to the PeerServer is established.
- *
- * You may use the peer before this is emitted, but messages to the server will be queued. <code>id</code> is the brokering ID of the peer (which was either provided in the constructor or assigned by the server).<span class='tip'>You should not wait for this event before connecting to other peers if connection speed is important.</span>
- */
- open: (id: string) => void;
- /**
- * Emitted when a new data connection is established from a remote peer.
- */
- connection: (dataConnection: DataConnection) => void;
- /**
- * Emitted when a remote peer attempts to call you.
- */
- call: (mediaConnection: MediaConnection) => void;
- /**
- * Emitted when the peer is destroyed and can no longer accept or create any new connections.
- */
- close: () => void;
- /**
- * Emitted when the peer is disconnected from the signalling server
- */
- disconnected: (currentId: string) => void;
- /**
- * Errors on the peer are almost always fatal and will destroy the peer.
- *
- * Errors from the underlying socket and PeerConnections are forwarded here.
- */
- error: (error: PeerError<`${PeerErrorType}`>) => void;
- }
- /**
- * A peer who can initiate connections with other peers.
- */
- export class Peer extends EventEmitterWithError<PeerErrorType, PeerEvents> {
- private static readonly DEFAULT_KEY = "peerjs";
- protected readonly _serializers: SerializerMapping = {
- raw: Raw,
- json: Json,
- binary: BinaryPack,
- "binary-utf8": BinaryPack,
- default: BinaryPack,
- };
- private readonly _options: PeerOptions;
- private readonly _api: API;
- private readonly _socket: Socket;
- private _id: string | null = null;
- private _lastServerId: string | null = null;
- // States.
- private _destroyed = false; // Connections have been killed
- private _disconnected = false; // Connection to PeerServer killed but P2P connections still active
- private _open = false; // Sockets and such are not yet open.
- private readonly _connections: Map<
- string,
- (DataConnection | MediaConnection)[]
- > = new Map(); // All connections for this peer.
- private readonly _lostMessages: Map<string, ServerMessage[]> = new Map(); // src => [list of messages]
- /**
- * The brokering ID of this peer
- *
- * If no ID was specified in {@apilink Peer | the constructor},
- * this will be `undefined` until the {@apilink PeerEvents | `open`} event is emitted.
- */
- get id() {
- return this._id;
- }
- get options() {
- return this._options;
- }
- get open() {
- return this._open;
- }
- /**
- * @internal
- */
- get socket() {
- return this._socket;
- }
- /**
- * A hash of all connections associated with this peer, keyed by the remote peer's ID.
- * @deprecated
- * Return type will change from Object to Map<string,[]>
- */
- get connections(): Object {
- const plainConnections = Object.create(null);
- for (const [k, v] of this._connections) {
- plainConnections[k] = v;
- }
- return plainConnections;
- }
- /**
- * true if this peer and all of its connections can no longer be used.
- */
- get destroyed() {
- return this._destroyed;
- }
- /**
- * false if there is an active connection to the PeerServer.
- */
- get disconnected() {
- return this._disconnected;
- }
- /**
- * A peer can connect to other peers and listen for connections.
- */
- constructor();
- /**
- * A peer can connect to other peers and listen for connections.
- * @param options for specifying details about PeerServer
- */
- constructor(options: PeerOptions);
- /**
- * A peer can connect to other peers and listen for connections.
- * @param id Other peers can connect to this peer using the provided ID.
- * If no ID is given, one will be generated by the brokering server.
- * The ID must start and end with an alphanumeric character (lower or upper case character or a digit). In the middle of the ID spaces, dashes (-) and underscores (_) are allowed. Use {@apilink PeerOptions.metadata } to send identifying information.
- * @param options for specifying details about PeerServer
- */
- constructor(id: string, options?: PeerOptions);
- constructor(id?: string | PeerOptions, options?: PeerOptions) {
- super();
- let userId: string | undefined;
- // Deal with overloading
- if (id && id.constructor == Object) {
- options = id as PeerOptions;
- } else if (id) {
- userId = id.toString();
- }
- // Configurize options
- options = {
- debug: 0, // 1: Errors, 2: Warnings, 3: All logs
- host: util.CLOUD_HOST,
- port: util.CLOUD_PORT,
- path: "/",
- key: Peer.DEFAULT_KEY,
- token: util.randomToken(),
- config: util.defaultConfig,
- referrerPolicy: "strict-origin-when-cross-origin",
- serializers: {},
- ...options,
- };
- this._options = options;
- this._serializers = { ...this._serializers, ...this.options.serializers };
- // Detect relative URL host.
- if (this._options.host === "/") {
- this._options.host = window.location.hostname;
- }
- // Set path correctly.
- if (this._options.path) {
- if (this._options.path[0] !== "/") {
- this._options.path = "/" + this._options.path;
- }
- if (this._options.path[this._options.path.length - 1] !== "/") {
- this._options.path += "/";
- }
- }
- // Set whether we use SSL to same as current host
- if (
- this._options.secure === undefined &&
- this._options.host !== util.CLOUD_HOST
- ) {
- this._options.secure = util.isSecure();
- } else if (this._options.host == util.CLOUD_HOST) {
- this._options.secure = true;
- }
- // Set a custom log function if present
- if (this._options.logFunction) {
- logger.setLogFunction(this._options.logFunction);
- }
- logger.logLevel = this._options.debug || 0;
- this._api = new API(options);
- this._socket = this._createServerConnection();
- // Sanity checks
- // Ensure WebRTC supported
- if (!util.supports.audioVideo && !util.supports.data) {
- this._delayedAbort(
- PeerErrorType.BrowserIncompatible,
- "The current browser does not support WebRTC",
- );
- return;
- }
- // Ensure alphanumeric id
- if (!!userId && !util.validateId(userId)) {
- this._delayedAbort(PeerErrorType.InvalidID, `ID "${userId}" is invalid`);
- return;
- }
- if (userId) {
- this._initialize(userId);
- } else {
- this._api
- .retrieveId()
- .then((id) => this._initialize(id))
- .catch((error) => this._abort(PeerErrorType.ServerError, error));
- }
- }
- private _createServerConnection(): Socket {
- const socket = new Socket(
- this._options.secure,
- this._options.host!,
- this._options.port!,
- this._options.path!,
- this._options.key!,
- this._options.pingInterval,
- );
- socket.on(SocketEventType.Message, (data: ServerMessage) => {
- this._handleMessage(data);
- });
- socket.on(SocketEventType.Error, (error: string) => {
- this._abort(PeerErrorType.SocketError, error);
- });
- socket.on(SocketEventType.Disconnected, () => {
- if (this.disconnected) {
- return;
- }
- this.emitError(PeerErrorType.Network, "Lost connection to server.");
- this.disconnect();
- });
- socket.on(SocketEventType.Close, () => {
- if (this.disconnected) {
- return;
- }
- this._abort(
- PeerErrorType.SocketClosed,
- "Underlying socket is already closed.",
- );
- });
- return socket;
- }
- /** Initialize a connection with the server. */
- private _initialize(id: string): void {
- this._id = id;
- this.socket.start(id, this._options.token!);
- }
- /** Handles messages from the server. */
- private _handleMessage(message: ServerMessage): void {
- const type = message.type;
- const payload = message.payload;
- const peerId = message.src;
- switch (type) {
- case ServerMessageType.Open: // The connection to the server is open.
- this._lastServerId = this.id;
- this._open = true;
- this.emit("open", this.id);
- break;
- case ServerMessageType.Error: // Server error.
- this._abort(PeerErrorType.ServerError, payload.msg);
- break;
- case ServerMessageType.IdTaken: // The selected ID is taken.
- this._abort(PeerErrorType.UnavailableID, `ID "${this.id}" is taken`);
- break;
- case ServerMessageType.InvalidKey: // The given API key cannot be found.
- this._abort(
- PeerErrorType.InvalidKey,
- `API KEY "${this._options.key}" is invalid`,
- );
- break;
- case ServerMessageType.Leave: // Another peer has closed its connection to this peer.
- logger.log(`Received leave message from ${peerId}`);
- this._cleanupPeer(peerId);
- this._connections.delete(peerId);
- break;
- case ServerMessageType.Expire: // The offer sent to a peer has expired without response.
- this.emitError(
- PeerErrorType.PeerUnavailable,
- `Could not connect to peer ${peerId}`,
- );
- break;
- case ServerMessageType.Offer: {
- // we should consider switching this to CALL/CONNECT, but this is the least breaking option.
- const connectionId = payload.connectionId;
- let connection = this.getConnection(peerId, connectionId);
- if (connection) {
- connection.close();
- logger.warn(
- `Offer received for existing Connection ID:${connectionId}`,
- );
- }
- // Create a new connection.
- if (payload.type === ConnectionType.Media) {
- const mediaConnection = new MediaConnection(peerId, this, {
- connectionId: connectionId,
- _payload: payload,
- metadata: payload.metadata,
- });
- connection = mediaConnection;
- this._addConnection(peerId, connection);
- this.emit("call", mediaConnection);
- } else if (payload.type === ConnectionType.Data) {
- const dataConnection = new this._serializers[payload.serialization](
- peerId,
- this,
- {
- connectionId: connectionId,
- _payload: payload,
- metadata: payload.metadata,
- label: payload.label,
- serialization: payload.serialization,
- reliable: payload.reliable,
- },
- );
- connection = dataConnection;
- this._addConnection(peerId, connection);
- this.emit("connection", dataConnection);
- } else {
- logger.warn(`Received malformed connection type:${payload.type}`);
- return;
- }
- // Find messages.
- const messages = this._getMessages(connectionId);
- for (const message of messages) {
- connection.handleMessage(message);
- }
- break;
- }
- default: {
- if (!payload) {
- logger.warn(
- `You received a malformed message from ${peerId} of type ${type}`,
- );
- return;
- }
- const connectionId = payload.connectionId;
- const connection = this.getConnection(peerId, connectionId);
- if (connection && connection.peerConnection) {
- // Pass it on.
- connection.handleMessage(message);
- } else if (connectionId) {
- // Store for possible later use
- this._storeMessage(connectionId, message);
- } else {
- logger.warn("You received an unrecognized message:", message);
- }
- break;
- }
- }
- }
- /** Stores messages without a set up connection, to be claimed later. */
- private _storeMessage(connectionId: string, message: ServerMessage): void {
- if (!this._lostMessages.has(connectionId)) {
- this._lostMessages.set(connectionId, []);
- }
- this._lostMessages.get(connectionId).push(message);
- }
- /**
- * Retrieve messages from lost message store
- * @internal
- */
- //TODO Change it to private
- public _getMessages(connectionId: string): ServerMessage[] {
- const messages = this._lostMessages.get(connectionId);
- if (messages) {
- this._lostMessages.delete(connectionId);
- return messages;
- }
- return [];
- }
- /**
- * Connects to the remote peer specified by id and returns a data connection.
- * @param peer The brokering ID of the remote peer (their {@apilink Peer.id}).
- * @param options for specifying details about Peer Connection
- */
- connect(peer: string, options: PeerConnectOption = {}): DataConnection {
- options = {
- serialization: "default",
- ...options,
- };
- if (this.disconnected) {
- logger.warn(
- "You cannot connect to a new Peer because you called " +
- ".disconnect() on this Peer and ended your connection with the " +
- "server. You can create a new Peer to reconnect, or call reconnect " +
- "on this peer if you believe its ID to still be available.",
- );
- this.emitError(
- PeerErrorType.Disconnected,
- "Cannot connect to new Peer after disconnecting from server.",
- );
- return;
- }
- const dataConnection = new this._serializers[options.serialization](
- peer,
- this,
- options,
- );
- this._addConnection(peer, dataConnection);
- return dataConnection;
- }
- /**
- * Calls the remote peer specified by id and returns a media connection.
- * @param peer The brokering ID of the remote peer (their peer.id).
- * @param stream The caller's media stream
- * @param options Metadata associated with the connection, passed in by whoever initiated the connection.
- */
- call(
- peer: string,
- stream: MediaStream,
- options: CallOption = {},
- ): MediaConnection {
- if (this.disconnected) {
- logger.warn(
- "You cannot connect to a new Peer because you called " +
- ".disconnect() on this Peer and ended your connection with the " +
- "server. You can create a new Peer to reconnect.",
- );
- this.emitError(
- PeerErrorType.Disconnected,
- "Cannot connect to new Peer after disconnecting from server.",
- );
- return;
- }
- if (!stream) {
- logger.error(
- "To call a peer, you must provide a stream from your browser's `getUserMedia`.",
- );
- return;
- }
- const mediaConnection = new MediaConnection(peer, this, {
- ...options,
- _stream: stream,
- });
- this._addConnection(peer, mediaConnection);
- return mediaConnection;
- }
- /** Add a data/media connection to this peer. */
- private _addConnection(
- peerId: string,
- connection: MediaConnection | DataConnection,
- ): void {
- logger.log(
- `add connection ${connection.type}:${connection.connectionId} to peerId:${peerId}`,
- );
- if (!this._connections.has(peerId)) {
- this._connections.set(peerId, []);
- }
- this._connections.get(peerId).push(connection);
- }
- //TODO should be private
- _removeConnection(connection: DataConnection | MediaConnection): void {
- const connections = this._connections.get(connection.peer);
- if (connections) {
- const index = connections.indexOf(connection);
- if (index !== -1) {
- connections.splice(index, 1);
- }
- }
- //remove from lost messages
- this._lostMessages.delete(connection.connectionId);
- }
- /** Retrieve a data/media connection for this peer. */
- getConnection(
- peerId: string,
- connectionId: string,
- ): null | DataConnection | MediaConnection {
- const connections = this._connections.get(peerId);
- if (!connections) {
- return null;
- }
- for (const connection of connections) {
- if (connection.connectionId === connectionId) {
- return connection;
- }
- }
- return null;
- }
- private _delayedAbort(type: PeerErrorType, message: string | Error): void {
- setTimeout(() => {
- this._abort(type, message);
- }, 0);
- }
- /**
- * Emits an error message and destroys the Peer.
- * The Peer is not destroyed if it's in a disconnected state, in which case
- * it retains its disconnected state and its existing connections.
- */
- private _abort(type: PeerErrorType, message: string | Error): void {
- logger.error("Aborting!");
- this.emitError(type, message);
- if (!this._lastServerId) {
- this.destroy();
- } else {
- this.disconnect();
- }
- }
- /**
- * Destroys the Peer: closes all active connections as well as the connection
- * to the server.
- *
- * :::caution
- * This cannot be undone; the respective peer object will no longer be able
- * to create or receive any connections, its ID will be forfeited on the server,
- * and all of its data and media connections will be closed.
- * :::
- */
- destroy(): void {
- if (this.destroyed) {
- return;
- }
- logger.log(`Destroy peer with ID:${this.id}`);
- this.disconnect();
- this._cleanup();
- this._destroyed = true;
- this.emit("close");
- }
- /** Disconnects every connection on this peer. */
- private _cleanup(): void {
- for (const peerId of this._connections.keys()) {
- this._cleanupPeer(peerId);
- this._connections.delete(peerId);
- }
- this.socket.removeAllListeners();
- }
- /** Closes all connections to this peer. */
- private _cleanupPeer(peerId: string): void {
- const connections = this._connections.get(peerId);
- if (!connections) return;
- for (const connection of connections) {
- connection.close();
- }
- }
- /**
- * Disconnects the Peer's connection to the PeerServer. Does not close any
- * active connections.
- * Warning: The peer can no longer create or accept connections after being
- * disconnected. It also cannot reconnect to the server.
- */
- disconnect(): void {
- if (this.disconnected) {
- return;
- }
- const currentId = this.id;
- logger.log(`Disconnect peer with ID:${currentId}`);
- this._disconnected = true;
- this._open = false;
- this.socket.close();
- this._lastServerId = currentId;
- this._id = null;
- this.emit("disconnected", currentId);
- }
- /** Attempts to reconnect with the same ID.
- *
- * Only {@apilink Peer.disconnect | disconnected peers} can be reconnected.
- * Destroyed peers cannot be reconnected.
- * If the connection fails (as an example, if the peer's old ID is now taken),
- * the peer's existing connections will not close, but any associated errors events will fire.
- */
- reconnect(): void {
- if (this.disconnected && !this.destroyed) {
- logger.log(
- `Attempting reconnection to server with ID ${this._lastServerId}`,
- );
- this._disconnected = false;
- this._initialize(this._lastServerId!);
- } else if (this.destroyed) {
- throw new Error(
- "This peer cannot reconnect to the server. It has already been destroyed.",
- );
- } else if (!this.disconnected && !this.open) {
- // Do nothing. We're still connecting the first time.
- logger.error(
- "In a hurry? We're still trying to make the initial connection!",
- );
- } else {
- throw new Error(
- `Peer ${this.id} cannot reconnect because it is not disconnected from the server!`,
- );
- }
- }
- /**
- * Get a list of available peer IDs. If you're running your own server, you'll
- * want to set allow_discovery: true in the PeerServer options. If you're using
- * the cloud server, email team@peerjs.com to get the functionality enabled for
- * your key.
- */
- listAllPeers(cb = (_: any[]) => {}): void {
- this._api
- .listAllPeers()
- .then((peers) => cb(peers))
- .catch((error) => this._abort(PeerErrorType.ServerError, error));
- }
- }
|