// @ts-check
"use strict";
/** @typedef {{t: "identity", roomId: string}} IdentityMessage
*
* The first message sent by the server, gives your room ID and client ID in the message itself
*/
/** @typedef {{t: "sdp", sdp: {type: RTCSdpType, sdp: string}}} SdpMessage
*
* Message sent by a party in webrtc negotiation
*/
/** @typedef {{t: "ice", ice: RTCIceCandidateInit}} IceMessage
*
* Message sent by a party in webrtc negotiation
*/
/** @typedef {{t: "partyJoins"}} PartyJoinsMessage
*
* When sent by the server to the hosting party: signals that a new party
* joined to room to watch your stream.
*/
/** @typedef {{t: "joinRoom", roomId: string}} JoinRoomMessage
*
* When sent by the party to the server: signals wanting to leave the current
* room and join another.
*/
/** @typedef {{t: "leaveRoom"}} LeaveRoomMessage
*
* Indicates that the other end has closed the connection: either a watcher
* left, or a host closed the stream.
*/
/** @typedef {{t: "joinRoom", status: "success" | "404"}} JoinRoomResponse
*/
/**
* @typedef {Object} ServerMessage
* @property {string} senderId
* @property {IdentityMessage | SdpMessage | IceMessage | PartyJoinsMessage | JoinRoomResponse | LeaveRoomMessage} body
*/
/**
* @typedef {Object} ClientMessage
* @property {string | null} targetId
* @property {SdpMessage | IceMessage | JoinRoomMessage} body
*/
/**
* @param {unknown} x
* @returns {x is object}
*/
export const isObj = x => typeof x === "object" || x != null;
/**
* @param {unknown} x
* @returns {x is string}
*/
export const isString = x => typeof x === "string" || x instanceof String;
/**
* @param {unknown} x
* @returns {x is array}
*/
export const isArray = Array.isArray
/**
* @param {unknown} x
* @returns {x is RTCSdpType}
*/
export const isRtcSdptype = x => x === "answer" || x === "offer" || x === "pranswer" || x === "rollback";
/**
* @param {unknown} x
* @returns {x is RTCIceCandidateInit}
*
* This is stupid, but according to the api all the fields are optional. I
* might check that if the field is present it's set to a string..
*/
export const isRtcIceCandidateInit = isObj;
/**
* @param {string} jsonStr
* @returns {ServerMessage | null}
*/
export function decodeServerMessage(jsonStr) {
/** @type {unknown} */
const json = JSON.parse(jsonStr);
if (!isObj(json)) {
return null;
}
if (!("senderId" in json && isString(json.senderId))) {
return null;
}
if (!("body" in json && typeof json.body === "object" && json.body != null)) {
return null;
}
if (!("t" in json.body)) {
return null;
}
if (json.body.t === "sdp"
&& "sdp" in json.body && isObj(json.body.sdp)
&& "type" in json.body.sdp && isRtcSdptype(json.body.sdp.type)
&& "sdp" in json.body.sdp && isString(json.body.sdp.sdp)
) {
return {
senderId: json.senderId,
body: {
t: json.body.t,
sdp: {
type: json.body.sdp.type,
sdp: json.body.sdp.sdp,
},
},
};
} else if (json.body.t === "ice"
&& "ice" in json.body && isRtcIceCandidateInit(json.body.ice)
) {
return {
senderId: json.senderId,
body: {
t: json.body.t,
ice: json.body.ice,
},
};
} else if (json.body.t === "identity"
&& "roomId" in json.body && isString(json.body.roomId)
) {
return {
senderId: json.senderId,
body: {
t: json.body.t,
roomId: json.body.roomId,
},
};
} else if (json.body.t === "partyJoins") {
return {
senderId: json.senderId,
body: {
t: json.body.t,
},
};
} else if (json.body.t === "joinRoom"
&& "status" in json.body && (json.body.status === "success" || json.body.status === "404")
) {
return {
senderId: json.senderId,
body: {
t: json.body.t,
status: json.body.status,
},
};
} else if (json.body.t === "leaveRoom") {
return {
senderId: json.senderId,
body: {
t: json.body.t,
},
};
} else {
return null;
}
}
/**
* @param {ClientMessage} msg
* @returns {string}
*/
export function encodeServerMessage(msg) {
return JSON.stringify(msg);
}