@nodecg/types
Version:
Dynamic broadcast graphics rendered in a browser
358 lines • 14.6 kB
JavaScript
import { NodeCGAPIBase } from "../../shared/api.base";
import { filteredConfig } from "./config";
import { Logger } from "./logger";
import { ClientReplicant } from "./replicant";
const soundMetadata = new WeakMap();
const apiContexts = new Set();
/**
* This is what enables intra-context messaging.
* I.e., passing messages from one extension to another in the same Node.js context.
*/
function _forwardMessageToContext(messageName, bundleName, data) {
setTimeout(() => {
apiContexts.forEach((ctx) => {
ctx._messageHandlers.forEach((handler) => {
if (messageName === handler.messageName &&
bundleName === handler.bundleName) {
handler.func(data);
}
});
});
}, 0);
}
export class NodeCGAPIClient extends NodeCGAPIBase {
static Replicant(name, namespace, opts = {}) {
return new ClientReplicant(name, namespace, opts, globalThis.socket);
}
static sendMessageToBundle(messageName, bundleName, dataOrCb, cb) {
let data = null;
if (typeof dataOrCb === "function") {
cb = dataOrCb;
}
else {
data = dataOrCb;
}
_forwardMessageToContext(messageName, bundleName, data);
if (typeof cb === "function") {
globalThis.socket.emit("message", {
bundleName,
messageName,
content: data,
}, (err, response) => {
if (response) {
cb(err, response);
}
else {
cb(err);
}
});
}
else {
return new Promise((resolve, reject) => {
globalThis.socket.emit("message", {
bundleName,
messageName,
content: data,
}, (err, response) => {
if (err) {
reject(err);
}
else {
resolve(response);
}
});
});
}
}
static readReplicant(name, namespace, cb) {
globalThis.socket.emit("replicant:read", { name, namespace }, (error, value) => {
if (error) {
console.error(error);
}
else {
cb(value);
}
});
}
get Logger() {
return Logger;
}
get log() {
if (this._memoizedLogger) {
return this._memoizedLogger;
}
this._memoizedLogger = new Logger(this.bundleName);
return this._memoizedLogger;
}
/**
* A filtered copy of the NodeCG server config with some sensitive keys removed.
*/
get config() {
return Object.freeze(JSON.parse(JSON.stringify(filteredConfig)));
}
constructor(bundle, socket) {
var _a;
super(bundle);
this.soundsReady = false;
this._soundCues = [];
this._replicantFactory = (name, namespace, opts) => new ClientReplicant(name, namespace, opts, this.socket);
apiContexts.add(this);
// If title isn't set, set it to the bundle name
if (globalThis.document) {
document.addEventListener("DOMContentLoaded", () => {
if (document.title === "") {
document.title = this.bundleName;
}
}, false);
}
// Make socket accessible to public methods
this.socket = socket;
// eslint-disable-next-line @typescript-eslint/no-empty-function
this.socket.emit("joinRoom", bundle.name, () => { });
if (globalThis.window && bundle._hasSounds && ((_a = window.createjs) === null || _a === void 0 ? void 0 : _a.Sound)) {
const soundCuesRep = new ClientReplicant("soundCues", this.bundleName, {}, socket);
this._soundFiles = new ClientReplicant("assets:sounds", this.bundleName, {}, socket);
this._bundleVolume = new ClientReplicant(`volume:${this.bundleName}`, "_sounds", {}, socket);
this._masterVolume = new ClientReplicant("volume:master", "_sounds", {}, socket);
this._soundCues = [];
const loadedSums = new Set();
window.createjs.Sound.on("fileload", (e) => {
if (this.soundsReady || !e.data.sum) {
return;
}
loadedSums.add(e.data.sum);
const foundUnloaded = this._soundCues.some((cue) => {
if (cue.file) {
return !loadedSums.has(cue.file.sum);
}
return false;
});
if (!foundUnloaded && !this.soundsReady) {
this.soundsReady = true;
window.dispatchEvent(new CustomEvent("ncgSoundsReady"));
}
});
const handleAnyCuesRepChange = () => {
var _a;
this._soundCues = (_a = soundCuesRep.value) !== null && _a !== void 0 ? _a : [];
this._updateInstanceVolumes();
this._registerSounds();
};
soundCuesRep.on("change", handleAnyCuesRepChange);
this._soundFiles.on("change", () => this._registerSounds.bind(this));
this._bundleVolume.on("change", () => this._updateInstanceVolumes.bind(this));
this._masterVolume.on("change", () => this._updateInstanceVolumes.bind(this));
}
// Upon receiving a message, execute any handlers for it
socket.on("message", (data) => {
this.log.trace("Received message %s (sent to bundle %s) with data:", data.messageName, data.bundleName, data.content);
this._messageHandlers.forEach((handler) => {
if (data.messageName === handler.messageName &&
data.bundleName === handler.bundleName) {
handler.func(data.content);
}
});
});
socket.io.on("error", (err) => {
this.log.warn("Unhandled socket connection error:", err);
});
socket.on("protocol_error", (err) => {
if (err.type === "UnauthorizedError") {
if (globalThis.window) {
const url = [
location.protocol,
"//",
location.host,
location.pathname,
].join("");
window.location.href = `/authError?code=${err.code}&message=${err.message}&viewUrl=${url}`;
}
else {
globalThis.close();
}
}
else {
this.log.error("Unhandled socket protocol error:", err);
}
});
}
/**
* _Browser only, except workers_<br/>
* Returns the specified dialog element.
* @param {string} name - The desired dialog's name.
* @param {string} [bundle=CURR_BNDL] - The bundle from which to select the dialog.
* @returns {object}
*/
getDialog(name, bundle) {
var _a, _b, _c, _d;
if (!globalThis.window) {
return undefined;
}
bundle = bundle !== null && bundle !== void 0 ? bundle : this.bundleName;
const topDoc = (_a = window.top) === null || _a === void 0 ? void 0 : _a.document;
if (!topDoc) {
return undefined;
}
const dialog = (_c = (_b = topDoc
.querySelector("ncg-dashboard")) === null || _b === void 0 ? void 0 : _b.shadowRoot) === null || _c === void 0 ? void 0 : _c.querySelector(`#dialogs #${bundle}_${name}`);
return (_d = dialog) !== null && _d !== void 0 ? _d : undefined;
}
/**
* _Browser only, except workers_<br/>
* Returns the specified dialog's iframe document.
* @param {string} name - The desired dialog's name.
* @param {string} [bundle=CURR_BNDL] - The bundle from which to select the dialog.
* @returns {object}
*/
getDialogDocument(name, bundle) {
var _a, _b, _c;
if (!globalThis.window) {
return undefined;
}
bundle = bundle !== null && bundle !== void 0 ? bundle : this.bundleName;
return (_c = (_b = (_a = this.getDialog(name, bundle)) === null || _a === void 0 ? void 0 : _a.querySelector("iframe")) === null || _b === void 0 ? void 0 : _b.contentWindow) === null || _c === void 0 ? void 0 : _c.document;
}
/**
* Returns the sound cue of the provided `cueName` in the current bundle.
* Returns undefined if a cue by that name cannot be found in this bundle.
*/
findCue(cueName) {
return this._soundCues.find((cue) => cue.name === cueName);
}
/**
* Plays the sound cue of the provided `cueName` in the current bundle.
* Does nothing if the cue doesn't exist or if the cue has no assigned file to play.
* @param cueName {String}
* @param [opts] {Object}
* @param [opts.updateVolume=true] - Whether or not to let NodeCG automatically update this instance's volume
* when the user changes it on the dashboard.
* @returns {Object|undefined} - A SoundJS AbstractAudioInstance.
*/
playSound(cueName, { updateVolume = true } = { updateVolume: true }) {
var _a;
if (!globalThis.window) {
throw new Error("NodeCG Sound API methods are not available in workers");
}
if (!this._soundCues) {
throw new Error(`Bundle "${this.bundleName}" has no soundCues`);
}
const cue = this.findCue(cueName);
if (!cue) {
throw new Error(`Cue "${cueName}" does not exist in bundle "${this.bundleName}"`);
}
if (!((_a = window.createjs) === null || _a === void 0 ? void 0 : _a.Sound)) {
throw new Error("NodeCG Sound API methods are not available when SoundJS isn't present");
}
if (!cue.file) {
throw new Error(`Cue $"{cueName}" does not have a file specified.`);
}
// Create an instance of the sound, which begins playing immediately.
const instance = window.createjs.Sound.play(cueName);
// Set the volume.
this._setInstanceVolume(instance, cue);
soundMetadata.set(instance, { cueName, updateVolume });
return instance;
}
/**
* Stops all currently playing instances of the provided `cueName`.
* @param cueName {String}
*/
stopSound(cueName) {
var _a;
if (!globalThis.window) {
throw new Error("NodeCG Sound API methods are not available in workers");
}
if (!this._soundCues) {
throw new Error(`Bundle "${this.bundleName}" has no soundCues`);
}
if (!this._soundCues.find((cue) => cue.name === cueName)) {
throw new Error(`Cue "${cueName}" does not exist in bundle "${this.bundleName}"`);
}
if (!((_a = window.createjs) === null || _a === void 0 ? void 0 : _a.Sound)) {
throw new Error("NodeCG Sound API methods are not available when SoundJS isn't present");
}
const instancesArr = window.createjs.Sound
._instances;
for (let i = instancesArr.length - 1; i >= 0; i--) {
const instance = instancesArr[i];
const meta = soundMetadata.get(instance);
if (meta && meta.cueName === cueName) {
instance.stop();
}
}
}
/**
* Stops all currently playing sounds on the page.
*/
stopAllSounds() {
var _a;
if (!globalThis.window) {
throw new Error("NodeCG Sound API methods are not available in workers");
}
if (!((_a = window.createjs) === null || _a === void 0 ? void 0 : _a.Sound)) {
throw new Error("NodeCG Sound API methods are not available when SoundJS isn't present");
}
window.createjs.Sound.stop();
}
sendMessageToBundle(messageName, bundleName, dataOrCb, cb) {
return NodeCGAPIClient.sendMessageToBundle(messageName, bundleName, dataOrCb, cb);
}
sendMessage(messageName, dataOrCb, cb) {
return this.sendMessageToBundle(messageName, this.bundleName, dataOrCb, cb);
}
readReplicant(name, nspOrCb, maybeCb) {
let namespace = this.bundleName;
let cb;
if (typeof nspOrCb === "string") {
namespace = nspOrCb;
cb = maybeCb;
}
else {
cb = nspOrCb;
}
NodeCGAPIClient.readReplicant(name, namespace, cb);
}
_registerSounds() {
if (!globalThis.window) {
throw new Error("NodeCG Sound API methods are not available in workers");
}
this._soundCues.forEach((cue) => {
if (!cue.file) {
return;
}
window.createjs.Sound.registerSound(`${cue.file.url}?sum=${cue.file.sum}`, cue.name, {
channels: typeof cue.channels === "undefined" ? 100 : cue.channels,
sum: cue.file.sum,
});
});
}
_setInstanceVolume(instance, cue) {
var _a, _b;
if (((_a = this._masterVolume) === null || _a === void 0 ? void 0 : _a.status) !== "declared" ||
typeof this._masterVolume.value !== "number" ||
((_b = this._bundleVolume) === null || _b === void 0 ? void 0 : _b.status) !== "declared" ||
typeof this._bundleVolume.value !== "number") {
return;
}
const volume = (this._masterVolume.value / 100) *
(this._bundleVolume.value / 100) *
(cue.volume / 100);
// Volue value must be finite or SoundJS throws error
instance.volume = isFinite(volume) ? volume : 0;
}
_updateInstanceVolumes() {
const instancesArr = window.createjs.Sound
._instances;
// Update the volume of any playing instances that haven't opted out of automatic volume updates.
this._soundCues.forEach((cue) => {
instancesArr.forEach((instance) => {
const meta = soundMetadata.get(instance);
if (meta && meta.cueName === cue.name && meta.updateVolume) {
this._setInstanceVolume(instance, cue);
}
});
});
}
}
globalThis.NodeCG = NodeCGAPIClient;
//# sourceMappingURL=api.client.js.map