255 lines
6.7 KiB
JavaScript
255 lines
6.7 KiB
JavaScript
|
|
/**
|
||
|
|
* @typedef {object} AVSettingsData
|
||
|
|
* @property {boolean} [muted] Whether this user has muted themselves.
|
||
|
|
* @property {boolean} [hidden] Whether this user has hidden their video.
|
||
|
|
* @property {boolean} [speaking] Whether the user is broadcasting audio.
|
||
|
|
*/
|
||
|
|
|
||
|
|
class AVSettings {
|
||
|
|
constructor() {
|
||
|
|
this.initialize();
|
||
|
|
this._set = foundry.utils.debounce((key, value) => game.settings.set("core", key, value), 100);
|
||
|
|
this._change = foundry.utils.debounce(this._onSettingsChanged.bind(this), 100);
|
||
|
|
this.activity[game.userId] = {};
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* WebRTC Mode, Disabled, Audio only, Video only, Audio & Video
|
||
|
|
* @enum {number}
|
||
|
|
*/
|
||
|
|
static AV_MODES = {
|
||
|
|
DISABLED: 0,
|
||
|
|
AUDIO: 1,
|
||
|
|
VIDEO: 2,
|
||
|
|
AUDIO_VIDEO: 3
|
||
|
|
};
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Voice modes: Always-broadcasting, voice-level triggered, push-to-talk.
|
||
|
|
* @enum {string}
|
||
|
|
*/
|
||
|
|
static VOICE_MODES = {
|
||
|
|
ALWAYS: "always",
|
||
|
|
ACTIVITY: "activity",
|
||
|
|
PTT: "ptt"
|
||
|
|
};
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Displayed nameplate options: Off entirely, animate between player and character name, player name only, character
|
||
|
|
* name only.
|
||
|
|
* @enum {number}
|
||
|
|
*/
|
||
|
|
static NAMEPLATE_MODES = {
|
||
|
|
OFF: 0,
|
||
|
|
BOTH: 1,
|
||
|
|
PLAYER_ONLY: 2,
|
||
|
|
CHAR_ONLY: 3
|
||
|
|
};
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* AV dock positions.
|
||
|
|
* @enum {string}
|
||
|
|
*/
|
||
|
|
static DOCK_POSITIONS = {
|
||
|
|
TOP: "top",
|
||
|
|
RIGHT: "right",
|
||
|
|
BOTTOM: "bottom",
|
||
|
|
LEFT: "left"
|
||
|
|
};
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Default client AV settings.
|
||
|
|
* @type {object}
|
||
|
|
*/
|
||
|
|
static DEFAULT_CLIENT_SETTINGS = {
|
||
|
|
videoSrc: "default",
|
||
|
|
audioSrc: "default",
|
||
|
|
audioSink: "default",
|
||
|
|
dockPosition: AVSettings.DOCK_POSITIONS.LEFT,
|
||
|
|
hidePlayerList: false,
|
||
|
|
hideDock: false,
|
||
|
|
muteAll: false,
|
||
|
|
disableVideo: false,
|
||
|
|
borderColors: false,
|
||
|
|
dockWidth: 240,
|
||
|
|
nameplates: AVSettings.NAMEPLATE_MODES.BOTH,
|
||
|
|
voice: {
|
||
|
|
mode: AVSettings.VOICE_MODES.PTT,
|
||
|
|
pttName: "`",
|
||
|
|
pttDelay: 100,
|
||
|
|
activityThreshold: -45
|
||
|
|
},
|
||
|
|
users: {}
|
||
|
|
};
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Default world-level AV settings.
|
||
|
|
* @type {object}
|
||
|
|
*/
|
||
|
|
static DEFAULT_WORLD_SETTINGS = {
|
||
|
|
mode: AVSettings.AV_MODES.DISABLED,
|
||
|
|
turn: {
|
||
|
|
type: "server",
|
||
|
|
url: "",
|
||
|
|
username: "",
|
||
|
|
password: ""
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Default client settings for each connected user.
|
||
|
|
* @type {object}
|
||
|
|
*/
|
||
|
|
static DEFAULT_USER_SETTINGS = {
|
||
|
|
popout: false,
|
||
|
|
x: 100,
|
||
|
|
y: 100,
|
||
|
|
z: 0,
|
||
|
|
width: 320,
|
||
|
|
volume: 1.0,
|
||
|
|
muted: false,
|
||
|
|
hidden: false,
|
||
|
|
blocked: false
|
||
|
|
};
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Stores the transient AV activity data received from other users.
|
||
|
|
* @type {Record<string, AVSettingsData>}
|
||
|
|
*/
|
||
|
|
activity = {};
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
initialize() {
|
||
|
|
this.client = game.settings.get("core", "rtcClientSettings");
|
||
|
|
this.world = game.settings.get("core", "rtcWorldSettings");
|
||
|
|
this._original = foundry.utils.deepClone({client: this.client, world: this.world});
|
||
|
|
const {muted, hidden} = this._getUserSettings(game.user);
|
||
|
|
game.user.broadcastActivity({av: {muted, hidden}});
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
changed() {
|
||
|
|
return this._change();
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
get(scope, setting) {
|
||
|
|
return foundry.utils.getProperty(this[scope], setting);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
getUser(userId) {
|
||
|
|
const user = game.users.get(userId);
|
||
|
|
if ( !user ) return null;
|
||
|
|
return this._getUserSettings(user);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
set(scope, setting, value) {
|
||
|
|
foundry.utils.setProperty(this[scope], setting, value);
|
||
|
|
this._set(`rtc${scope.titleCase()}Settings`, this[scope]);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Return a mapping of AV settings for each game User.
|
||
|
|
* @type {object}
|
||
|
|
*/
|
||
|
|
get users() {
|
||
|
|
const users = {};
|
||
|
|
for ( let u of game.users ) {
|
||
|
|
users[u.id] = this._getUserSettings(u);
|
||
|
|
}
|
||
|
|
return users;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* A helper to determine if the dock is configured in a vertical position.
|
||
|
|
*/
|
||
|
|
get verticalDock() {
|
||
|
|
const positions = this.constructor.DOCK_POSITIONS;
|
||
|
|
return [positions.LEFT, positions.RIGHT].includes(this.client.dockPosition ?? positions.LEFT);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Prepare a standardized object of user settings data for a single User
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
_getUserSettings(user) {
|
||
|
|
const clientSettings = this.client.users[user.id] || {};
|
||
|
|
const activity = this.activity[user.id] || {};
|
||
|
|
const settings = foundry.utils.mergeObject(AVSettings.DEFAULT_USER_SETTINGS, clientSettings, {inplace: false});
|
||
|
|
settings.canBroadcastAudio = user.can("BROADCAST_AUDIO");
|
||
|
|
settings.canBroadcastVideo = user.can("BROADCAST_VIDEO");
|
||
|
|
|
||
|
|
if ( user.isSelf ) {
|
||
|
|
settings.muted ||= !game.webrtc?.client.isAudioEnabled();
|
||
|
|
settings.hidden ||= !game.webrtc?.client.isVideoEnabled();
|
||
|
|
} else {
|
||
|
|
// Either we have muted or hidden them, or they have muted or hidden themselves.
|
||
|
|
settings.muted ||= !!activity.muted;
|
||
|
|
settings.hidden ||= !!activity.hidden;
|
||
|
|
}
|
||
|
|
|
||
|
|
settings.speaking = activity.speaking;
|
||
|
|
return settings;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handle setting changes to either rctClientSettings or rtcWorldSettings.
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
_onSettingsChanged() {
|
||
|
|
const original = this._original;
|
||
|
|
this.initialize();
|
||
|
|
const changed = foundry.utils.diffObject(original, this._original);
|
||
|
|
game.webrtc.onSettingsChanged(changed);
|
||
|
|
Hooks.callAll("rtcSettingsChanged", this, changed);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handle another connected user changing their AV settings.
|
||
|
|
* @param {string} userId
|
||
|
|
* @param {AVSettingsData} settings
|
||
|
|
*/
|
||
|
|
handleUserActivity(userId, settings) {
|
||
|
|
const current = this.activity[userId] || {};
|
||
|
|
this.activity[userId] = foundry.utils.mergeObject(current, settings, {inplace: false});
|
||
|
|
if ( !ui.webrtc ) return;
|
||
|
|
const hiddenChanged = ("hidden" in settings) && (current.hidden !== settings.hidden);
|
||
|
|
const mutedChanged = ("muted" in settings) && (current.muted !== settings.muted);
|
||
|
|
if ( (hiddenChanged || mutedChanged) && ui.webrtc.getUserVideoElement(userId) ) ui.webrtc._refreshView(userId);
|
||
|
|
if ( "speaking" in settings ) ui.webrtc.setUserIsSpeaking(userId, settings.speaking);
|
||
|
|
}
|
||
|
|
}
|