mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-06-27 21:05:53 +00:00
4.21.0
* Added: Setting to toggle the Audio Compressor with a shortcut key. * Added: Gain Control for the Audio Compressor. Gain Control is an additional volume control that runs after the compressor, as an alternative to the built-in volume control which happens before the compressor. * Changed: Use case-insensitive string comparison for the `Current Page` profile rule. * Fixed: The current channel not being detected correctly on user pages. * API Added: Standardized input processors for settings. So far, there are `to_int` and `to_float` with support for bounds.
This commit is contained in:
parent
66702103ff
commit
b3c3c8130d
20 changed files with 785 additions and 154 deletions
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "frankerfacez",
|
||||
"author": "Dan Salvato LLC",
|
||||
"version": "4.20.91",
|
||||
"version": "4.21.0",
|
||||
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
|
|
|
@ -33,13 +33,8 @@ export default class Actions extends Module {
|
|||
title: 'Action Size',
|
||||
description: "How tall actions should be, in pixels. This may be affected by your browser's zoom and font size settings.",
|
||||
component: 'setting-text-box',
|
||||
process(val) {
|
||||
val = parseInt(val, 10);
|
||||
if ( isNaN(val) || ! isFinite(val) || val <= 0 )
|
||||
return 16;
|
||||
|
||||
return val;
|
||||
}
|
||||
process: 'to_int',
|
||||
bounds: [1]
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -143,13 +143,8 @@ export default class Chat extends Module {
|
|||
title: 'Timestamp Font Size',
|
||||
description: 'How large should timestamps be, in pixels. Defaults to Font Size if not set.',
|
||||
component: 'setting-text-box',
|
||||
process(val) {
|
||||
val = parseInt(val, 10);
|
||||
if ( isNaN(val) || ! isFinite(val) || val <= 0 )
|
||||
return null;
|
||||
|
||||
return val;
|
||||
}
|
||||
process: 'to_int',
|
||||
bounds: [1]
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -160,13 +155,8 @@ export default class Chat extends Module {
|
|||
title: 'Font Size',
|
||||
description: "How large should text in chat be, in pixels. This may be affected by your browser's zoom and font size settings.",
|
||||
component: 'setting-text-box',
|
||||
process(val) {
|
||||
val = parseInt(val, 10);
|
||||
if ( isNaN(val) || ! isFinite(val) || val <= 0 )
|
||||
return 13;
|
||||
|
||||
return val;
|
||||
}
|
||||
process: 'to_int',
|
||||
bounds: [1]
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -252,13 +242,8 @@ export default class Chat extends Module {
|
|||
title: 'Scrollback Length',
|
||||
description: 'Keep up to this many lines in chat. Setting this too high will create lag.',
|
||||
component: 'setting-text-box',
|
||||
process(val) {
|
||||
val = parseInt(val, 10);
|
||||
if ( isNaN(val) || ! isFinite(val) || val < 1 )
|
||||
val = 150;
|
||||
|
||||
return val;
|
||||
}
|
||||
process: 'to_int',
|
||||
bounds: [1]
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -944,10 +929,7 @@ export default class Chat extends Module {
|
|||
description: 'Set the minimum contrast ratio used by Luma adjustments when determining readability.',
|
||||
|
||||
component: 'setting-text-box',
|
||||
|
||||
process(val) {
|
||||
return parseFloat(val)
|
||||
}
|
||||
process: 'to_float'
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
:type="type"
|
||||
:placeholder="placeholder"
|
||||
:value="value"
|
||||
:class="{'ffz-input--error': ! isValid}"
|
||||
class="tw-border-radius-medium tw-font-size-6 tw-pd-x-1 tw-pd-y-05 tw-mg-05 ffz-input"
|
||||
@change="onChange"
|
||||
>
|
||||
|
|
|
@ -848,6 +848,9 @@ export default class MainMenu extends Module {
|
|||
|
||||
deleteProfile: profile => settings.deleteProfile(profile),
|
||||
|
||||
getProcessor: key => settings.getProcessor(key),
|
||||
getValidator: key => settings.getValidator(key),
|
||||
|
||||
getFFZ: () => t.resolve('core'),
|
||||
|
||||
provider: {
|
||||
|
@ -927,7 +930,7 @@ export default class MainMenu extends Module {
|
|||
},
|
||||
|
||||
_context_changed() {
|
||||
this.context = deep_copy(context._context);
|
||||
this.context = deep_copy(context.__context);
|
||||
const profiles = context.manager.__profiles,
|
||||
ids = this.profiles = context.__profiles.map(profile => profile.id);
|
||||
|
||||
|
|
|
@ -59,6 +59,10 @@ export default {
|
|||
return deep_copy(this.item.default);
|
||||
},
|
||||
|
||||
isValid() {
|
||||
return this.isDefault || this.validate(this.value)
|
||||
},
|
||||
|
||||
isDefault() {
|
||||
return ! this.has_value
|
||||
}
|
||||
|
@ -78,13 +82,38 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
set(value) {
|
||||
if ( this.item.process )
|
||||
value = this.item.process(value);
|
||||
validate(value) {
|
||||
let validate = this.item.validator;
|
||||
if ( ! validate && typeof this.item.process === 'string' )
|
||||
validate = this.context.getValidator(`process_${this.item.process}`);
|
||||
if ( validate ) {
|
||||
if ( typeof validate !== 'function' )
|
||||
validate = this.context.getValidator(validate);
|
||||
if ( typeof validate === 'function' )
|
||||
return validate(value, this.item, this);
|
||||
else
|
||||
throw new Error(`Invalid Validator for ${this.item.setting}`);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
set(value) {
|
||||
const provider = this.context.provider,
|
||||
setting = this.item.setting;
|
||||
|
||||
// TODO: Run validation.
|
||||
|
||||
let process = this.item.process;
|
||||
if ( process ) {
|
||||
if ( typeof process !== 'function' )
|
||||
process = this.context.getProcessor(process);
|
||||
if ( typeof process === 'function' )
|
||||
value = process(value, this.default_value, this.item, this);
|
||||
else
|
||||
throw new Error(`Invalid processor for ${setting}`);
|
||||
}
|
||||
|
||||
provider.set(setting, value);
|
||||
this.has_value = true;
|
||||
this.value = deep_copy(value);
|
||||
|
|
|
@ -93,10 +93,7 @@ export default {
|
|||
},
|
||||
|
||||
isValid() {
|
||||
if ( typeof this.item.validator === 'function' )
|
||||
return this.item.validator(this.value, this);
|
||||
|
||||
return true;
|
||||
return this.isDefault || this.validate(this.value);
|
||||
},
|
||||
|
||||
sourceOrder() {
|
||||
|
@ -195,9 +192,34 @@ export default {
|
|||
}, 0);
|
||||
},
|
||||
|
||||
validate(value) {
|
||||
let validate = this.item.validator;
|
||||
if ( ! validate && typeof this.item.process === 'string' )
|
||||
validate = this.context.getValidator(`process_${this.item.process}`);
|
||||
if ( validate ) {
|
||||
if ( typeof validate !== 'function' )
|
||||
validate = this.context.getValidator(validate);
|
||||
if ( typeof validate === 'function' )
|
||||
return validate(value, this.item, this);
|
||||
else
|
||||
throw new Error(`Invalid Validator for ${this.item.setting}`);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
set(value) {
|
||||
if ( this.item.process )
|
||||
value = this.item.process(value);
|
||||
// TODO: Run validation.
|
||||
|
||||
let process = this.item.process;
|
||||
if ( process ) {
|
||||
if ( typeof process !== 'function' )
|
||||
process = this.context.getProcessor(process);
|
||||
if ( typeof process === 'function' )
|
||||
value = process(value, this.default_value, this.item, this);
|
||||
else
|
||||
throw new Error(`Invalid processor for ${this.item.setting}`);
|
||||
}
|
||||
|
||||
this.profile.set(this.item.setting, value);
|
||||
|
||||
|
|
|
@ -60,13 +60,8 @@ export default class Metadata extends Module {
|
|||
description: 'When the current stream delay exceeds this number of seconds, display the stream delay in a warning color to draw attention to the large delay. Set to zero to disable.',
|
||||
|
||||
component: 'setting-text-box',
|
||||
process(val) {
|
||||
val = parseInt(val, 10);
|
||||
if ( isNaN(val) || ! isFinite(val) || val < 0 )
|
||||
return 0;
|
||||
|
||||
return val;
|
||||
},
|
||||
process: 'to_float',
|
||||
bounds: [0, true]
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -248,7 +248,7 @@ export const Page = {
|
|||
if ( typeof part === 'object' ) {
|
||||
const val = config.values[part.name];
|
||||
if ( val && val.length )
|
||||
parts.push([i, val]);
|
||||
parts.push([i, val.toLowerCase()]);
|
||||
|
||||
i++;
|
||||
}
|
||||
|
@ -262,9 +262,14 @@ export const Page = {
|
|||
if ( ! ctx.route || ! ctx.route_data || ctx.route.name !== name )
|
||||
return false;
|
||||
|
||||
for(const [index, value] of parts)
|
||||
if ( ctx.route_data[index] !== value )
|
||||
for(const [index, value] of parts) {
|
||||
let thing = ctx.route_data[index];
|
||||
if ( typeof thing === 'string' )
|
||||
thing = thing.toLowerCase();
|
||||
|
||||
if ( thing !== value )
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -12,6 +12,8 @@ import SettingsProfile from './profile';
|
|||
import SettingsContext from './context';
|
||||
import MigrationManager from './migration';
|
||||
|
||||
import * as PROCESSORS from './processors';
|
||||
import * as VALIDATORS from './validators';
|
||||
import * as PROVIDERS from './providers';
|
||||
import * as FILTERS from './filters';
|
||||
import * as CLEARABLES from './clearables';
|
||||
|
@ -77,6 +79,20 @@ export default class SettingsManager extends Module {
|
|||
this.ui_structures = new Map;
|
||||
this.definitions = new Map;
|
||||
|
||||
// Validators
|
||||
this.validators = {};
|
||||
|
||||
for(const key in VALIDATORS)
|
||||
if ( has(VALIDATORS, key) )
|
||||
this.validators[key] = VALIDATORS[key];
|
||||
|
||||
// Processors
|
||||
this.processors = {};
|
||||
|
||||
for(const key in PROCESSORS)
|
||||
if ( has(PROCESSORS, key) )
|
||||
this.processors[key] = PROCESSORS[key];
|
||||
|
||||
// Clearable Data Rules
|
||||
this.clearables = {};
|
||||
|
||||
|
@ -927,6 +943,46 @@ export default class SettingsManager extends Module {
|
|||
getClearables() {
|
||||
return deep_copy(this.clearables);
|
||||
}
|
||||
|
||||
|
||||
addProcessor(key, fn) {
|
||||
if ( typeof key === 'object' ) {
|
||||
for(const k in key)
|
||||
if ( has(key, k) )
|
||||
this.addProcessor(k, key[k]);
|
||||
return;
|
||||
}
|
||||
|
||||
this.processors[key] = fn;
|
||||
}
|
||||
|
||||
getProcessor(key) {
|
||||
return this.processors[key];
|
||||
}
|
||||
|
||||
getProcessors() {
|
||||
return deep_copy(this.processors);
|
||||
}
|
||||
|
||||
|
||||
addValidator(key, fn) {
|
||||
if ( typeof key === 'object' ) {
|
||||
for(const k in key)
|
||||
if ( has(key, k) )
|
||||
this.addValidator(k, key[k]);
|
||||
return;
|
||||
}
|
||||
|
||||
this.validators[key] = fn;
|
||||
}
|
||||
|
||||
getValidator(key) {
|
||||
return this.validators[key];
|
||||
}
|
||||
|
||||
getValidators() {
|
||||
return deep_copy(this.validators);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
49
src/settings/processors.js
Normal file
49
src/settings/processors.js
Normal file
|
@ -0,0 +1,49 @@
|
|||
'use strict';
|
||||
|
||||
const BAD = Symbol('BAD');
|
||||
|
||||
const do_number = (val, default_value, def) => {
|
||||
if ( typeof val !== 'number' || isNaN(val) || ! isFinite(val) )
|
||||
val = BAD;
|
||||
|
||||
if ( val !== BAD ) {
|
||||
const bounds = def.bounds;
|
||||
if ( Array.isArray(bounds) ) {
|
||||
if ( bounds.length >= 3 ) {
|
||||
// [low, inclusive, high, inclusive]
|
||||
if ( (bounds[1] ? (val < bounds[0]) : (val <= bounds[0])) ||
|
||||
(bounds[3] ? (val > bounds[2]) : (val >= bounds[2])) )
|
||||
val = BAD;
|
||||
|
||||
} else if ( bounds.length === 2 ) {
|
||||
// [low, inclusive] or [low, high] ?
|
||||
if ( typeof bounds[1] === 'boolean' ) {
|
||||
if ( bounds[1] ? val < bounds[0] : val <= bounds[0] )
|
||||
val = BAD;
|
||||
} else if ( val < bounds[0] || val > bounds[1] )
|
||||
val = BAD;
|
||||
} else if ( bounds.length === 1 && val < bounds[0] )
|
||||
val = BAD;
|
||||
}
|
||||
}
|
||||
|
||||
return val === BAD ? default_value : val;
|
||||
}
|
||||
|
||||
export const to_int = (val, default_value, def) => {
|
||||
if ( typeof val === 'string' && ! /^-?\d+$/.test(val) )
|
||||
val = BAD;
|
||||
else
|
||||
val = parseInt(val, 10);
|
||||
|
||||
return do_number(val, default_value, def);
|
||||
}
|
||||
|
||||
export const to_float = (val, default_value, def) => {
|
||||
if ( typeof val === 'string' && ! /^-?[\d.]+$/.test(val) )
|
||||
val = BAD;
|
||||
else
|
||||
val = parseFloat(val);
|
||||
|
||||
return do_number(val, default_value, def);
|
||||
}
|
|
@ -5,33 +5,11 @@
|
|||
// ============================================================================
|
||||
|
||||
import {EventEmitter} from 'utilities/events';
|
||||
import {has} from 'utilities/object';
|
||||
import {isValidShortcut, has} from 'utilities/object';
|
||||
import {createTester} from 'utilities/filtering';
|
||||
|
||||
const fetchJSON = (url, options) => fetch(url, options).then(r => r.ok ? r.json() : null).catch(() => null);
|
||||
|
||||
// TODO: Move this into its own file.
|
||||
const BAD_SHORTCUTS = [
|
||||
'f',
|
||||
'space',
|
||||
'k',
|
||||
'shift+up',
|
||||
'shift+down',
|
||||
'esc',
|
||||
'm',
|
||||
'?',
|
||||
'alt+t',
|
||||
'alt+x'
|
||||
];
|
||||
|
||||
function isValidShortcut(key) {
|
||||
if ( ! key )
|
||||
return false;
|
||||
|
||||
key = key.toLowerCase().trim();
|
||||
return ! BAD_SHORTCUTS.includes(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instances of SettingsProfile are used for getting and setting raw settings
|
||||
* values, enumeration, and emit events when the raw settings are changed.
|
||||
|
|
45
src/settings/validators.js
Normal file
45
src/settings/validators.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
'use strict';
|
||||
|
||||
const do_number = (val, def) => {
|
||||
if ( typeof val !== 'number' || isNaN(val) || ! isFinite(val) )
|
||||
return false;
|
||||
|
||||
const bounds = def.bounds;
|
||||
if ( Array.isArray(bounds) ) {
|
||||
if ( bounds.length >= 3 ) {
|
||||
// [low, inclusive, high, inclusive]
|
||||
if ( (bounds[1] ? (val < bounds[0]) : (val <= bounds[0])) ||
|
||||
(bounds[3] ? (val > bounds[2]) : (val >= bounds[2])) )
|
||||
return false;
|
||||
|
||||
} else if ( bounds.length === 2 ) {
|
||||
// [low, inclusive] or [low, high] ?
|
||||
if ( typeof bounds[1] === 'boolean' ) {
|
||||
if ( bounds[1] ? val < bounds[0] : val <= bounds[0] )
|
||||
return false;
|
||||
} else if ( val < bounds[0] || val > bounds[1] )
|
||||
return false;
|
||||
} else if ( bounds.length === 1 && val < bounds[0] )
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export const process_to_int = (val, def) => {
|
||||
if ( typeof val === 'string' && ! /^-?\d+$/.test(val) )
|
||||
return false;
|
||||
else
|
||||
val = parseInt(val, 10);
|
||||
|
||||
return do_number(val, def);
|
||||
}
|
||||
|
||||
export const process_to_float = (val, def) => {
|
||||
if ( typeof val === 'string' && ! /^-?[\d.]+$/.test(val) )
|
||||
return false;
|
||||
else
|
||||
val = parseFloat(val);
|
||||
|
||||
return do_number(val, def);
|
||||
}
|
|
@ -45,6 +45,7 @@ export default class ClipsSite extends BaseSite {
|
|||
|
||||
this.css_tweaks.rules = {
|
||||
'unfollow-button': '.follow-btn__follow-btn--following,.follow-btn--following',
|
||||
'player-gain-volume': '.video-player__overlay[data-compressed="true"] .volume-slider__slider-container:not(.ffz--player-gain)',
|
||||
'player-ext': '.video-player .extension-taskbar,.video-player .extension-container,.video-player .extensions-dock__layout,.video-player .extensions-notifications,.video-player .extensions-video-overlay-size-container,.video-player .extensions-dock__layout',
|
||||
'player-ext-hover': '.video-player__overlay[data-controls="false"] .extension-taskbar,.video-player__overlay[data-controls="false"] .extension-container,.video-player__overlay[data-controls="false"] .extensions-dock__layout,.video-player__overlay[data-controls="false"] .extensions-notifications,.video-player__overlay[data-controls="false"] .extensions-video-overlay-size-container',
|
||||
'dark-toggle': 'div[data-a-target="dark-mode-toggle"],div[data-a-target="dark-mode-toggle"] + .tw-border-b'
|
||||
|
|
|
@ -35,6 +35,7 @@ export default class PlayerSite extends BaseSite {
|
|||
|
||||
this.css_tweaks.rules = {
|
||||
'unfollow-button': '.follow-btn--following',
|
||||
'player-gain-volume': '.video-player__overlay[data-compressed="true"] .volume-slider__slider-container:not(.ffz--player-gain)',
|
||||
'player-ext': '.video-player .extension-taskbar,.video-player .extension-container,.video-player .extensions-dock__layout,.video-player .extensions-notifications,.video-player .extensions-video-overlay-size-container,.video-player .extensions-dock__layout',
|
||||
'player-ext-hover': '.video-player__overlay[data-controls="false"] .extension-taskbar,.video-player__overlay[data-controls="false"] .extension-container,.video-player__overlay[data-controls="false"] .extensions-dock__layout,.video-player__overlay[data-controls="false"] .extensions-notifications,.video-player__overlay[data-controls="false"] .extensions-video-overlay-size-container'
|
||||
};
|
||||
|
|
|
@ -7,12 +7,46 @@
|
|||
import Module from 'utilities/module';
|
||||
|
||||
import {createElement, on, off} from 'utilities/dom';
|
||||
import {debounce} from 'utilities/object';
|
||||
import {isValidShortcut, debounce} from 'utilities/object';
|
||||
import { IS_FIREFOX } from 'src/utilities/constants';
|
||||
|
||||
const STYLE_VALIDATOR = createElement('span');
|
||||
|
||||
const HAS_COMPRESSOR = window.AudioContext && window.DynamicsCompressorNode != null;
|
||||
const HAS_COMPRESSOR = window.AudioContext && window.DynamicsCompressorNode != null,
|
||||
HAS_GAIN = HAS_COMPRESSOR && window.GainNode != null;
|
||||
|
||||
const SCROLL_I18N = 'setting.entry.player.volume-scroll.values',
|
||||
SCROLL_OPTIONS = [
|
||||
{value: false, title: 'Disabled', i18n_key: `${SCROLL_I18N}.false`},
|
||||
{value: true, title: 'Enabled', i18n_key: `${SCROLL_I18N}.true`},
|
||||
{value: 2, title: 'Enabled with Right-Click', i18n_key: `${SCROLL_I18N}.2`},
|
||||
{value: 3, title: 'Enabled with Alt', i18n_key: `${SCROLL_I18N}.3`},
|
||||
{value: 4, title: 'Enabled with Alt + Right-Click', i18n_key: `${SCROLL_I18N}.4`},
|
||||
{value: 5, title: 'Enabled with Shift', i18n_key: `${SCROLL_I18N}.5`},
|
||||
{value: 6, title: 'Enabled with Shift + Right-Click', i18n_key: `${SCROLL_I18N}.6`},
|
||||
{value: 7, title: 'Enabled with Ctrl', i18n_key: `${SCROLL_I18N}.7`},
|
||||
{value: 8, title: 'Enabled with Ctrl + Right-Click', i18n_key: `${SCROLL_I18N}.8`}
|
||||
];
|
||||
|
||||
function wantsRMB(setting) {
|
||||
return setting === 2 || setting === 4 || setting === 6 || setting === 8;
|
||||
}
|
||||
|
||||
function matchesEvent(setting, event, has_rmb) {
|
||||
if ( wantsRMB(setting) && event.button !== 2 && ! has_rmb )
|
||||
return false;
|
||||
|
||||
if ( ! event.altKey && (setting === 3 || setting === 4) )
|
||||
return false;
|
||||
|
||||
if ( ! event.shiftKey && (setting === 5 || setting === 6) )
|
||||
return false;
|
||||
|
||||
if ( ! event.ctrlKey && (setting === 7 || setting === 8) )
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function rotateButton(event) {
|
||||
const target = event.currentTarget,
|
||||
|
@ -38,6 +72,8 @@ export default class PlayerBase extends Module {
|
|||
this.inject('site.fine');
|
||||
this.inject('site.css_tweaks');
|
||||
|
||||
this.onShortcut = this.onShortcut.bind(this);
|
||||
|
||||
this.registerSettings();
|
||||
}
|
||||
|
||||
|
@ -81,6 +117,136 @@ export default class PlayerBase extends Module {
|
|||
}
|
||||
});
|
||||
|
||||
this.settings.add('player.compressor.shortcut', {
|
||||
default: null,
|
||||
requires: ['player.compressor.enable'],
|
||||
process(ctx, val) {
|
||||
if ( ! ctx.get('player.compressor.enable') )
|
||||
return null;
|
||||
return val;
|
||||
},
|
||||
ui: {
|
||||
path: 'Player > Compressor >> General',
|
||||
title: 'Shortcut Key',
|
||||
description: 'This key sequence can be used to toggle the compressor.',
|
||||
component: 'setting-hotkey'
|
||||
},
|
||||
changed: () => {
|
||||
this.updateShortcut();
|
||||
for(const inst of this.Player.instances)
|
||||
this.addCompressorButton(inst);
|
||||
}
|
||||
});
|
||||
|
||||
if ( HAS_GAIN ) {
|
||||
this.settings.add('player.gain.enable', {
|
||||
default: false,
|
||||
ui: {
|
||||
sort: -1,
|
||||
path: 'Player > Compressor >> Gain Control @{"sort": 50, "description": "Gain Control gives you extra control over the output volume when using the Compressor by letting you adjust the volume after the compressor runs, while the built-in volume slider takes affect before the compressor. This uses a simple [GainNode](https://developer.mozilla.org/en-US/docs/Web/API/GainNode) from the Web Audio API, connected in sequence after the DynamicsCompressorNode the Compressor uses."}',
|
||||
title: 'Enable gain control when the audio compressor is enabled.',
|
||||
component: 'setting-check-box'
|
||||
},
|
||||
|
||||
changed: () => {
|
||||
for(const inst of this.Player.instances)
|
||||
this.compressPlayer(inst);
|
||||
}
|
||||
});
|
||||
|
||||
this.settings.add('player.gain.no-volume', {
|
||||
default: false,
|
||||
requires: ['player.gain.enable'],
|
||||
process(ctx, val) {
|
||||
if ( ! ctx.get('player.gain.enable') )
|
||||
return false;
|
||||
return val;
|
||||
},
|
||||
|
||||
ui: {
|
||||
path: 'Player > Compressor >> Gain Control',
|
||||
title: 'Force built-in volume to 100% when the audio compressor is enabled.',
|
||||
description: 'With this enabled, the built-in volume will be hidden and the Gain Control will be the only way to change volume.',
|
||||
component: 'setting-check-box'
|
||||
},
|
||||
|
||||
changed: val => {
|
||||
this.css_tweaks.toggleHide('player-gain-volume', val);
|
||||
for(const inst of this.Player.instances)
|
||||
this.updateGainVolume(inst);
|
||||
}
|
||||
});
|
||||
|
||||
this.settings.add('player.gain.scroll', {
|
||||
default: false,
|
||||
ui: {
|
||||
path: 'Player > Compressor >> Gain Control',
|
||||
title: 'Scroll Adjust',
|
||||
description: 'Adjust the gain by scrolling with the mouse wheel. This setting takes precedence over adjusting the volume by scrolling. *This setting will not work properly on streams with visible extensions when mouse interaction with extensions is allowed.*',
|
||||
component: 'setting-select-box',
|
||||
data: SCROLL_OPTIONS
|
||||
}
|
||||
});
|
||||
|
||||
this.settings.add('player.gain.default', {
|
||||
default: 100,
|
||||
requires: ['player.gain.min', 'player.gain.max'],
|
||||
process(ctx, val) {
|
||||
const min = ctx.get('player.gain.min'),
|
||||
max = ctx.get('player.gain.max');
|
||||
|
||||
val /= 100;
|
||||
|
||||
if ( val < min )
|
||||
val = min;
|
||||
if ( val > max )
|
||||
val = max;
|
||||
|
||||
return val;
|
||||
},
|
||||
ui: {
|
||||
path: 'Player > Compressor >> Gain Control',
|
||||
title: 'Default Value',
|
||||
component: 'setting-text-box',
|
||||
description: 'The default value for gain control, when gain control is enabled. 100% means no change in volume.',
|
||||
process: 'to_int',
|
||||
bounds: [0, true]
|
||||
},
|
||||
|
||||
changed: () => this.updateGains()
|
||||
});
|
||||
|
||||
this.settings.add('player.gain.min', {
|
||||
default: 0,
|
||||
process: (ctx, val) => val / 100,
|
||||
ui: {
|
||||
path: 'Player > Compressor >> Gain Control',
|
||||
title: 'Minimum',
|
||||
component: 'setting-text-box',
|
||||
description: '**Range:** 0 ~ 100\n\nThe minimum allowed value for gain control. 0% is effectively muted.',
|
||||
process: 'to_int',
|
||||
bounds: [0, true, 100, true]
|
||||
},
|
||||
|
||||
changed: () => this.updateGains()
|
||||
});
|
||||
|
||||
this.settings.add('player.gain.max', {
|
||||
default: 200,
|
||||
process: (ctx, val) => val / 100,
|
||||
ui: {
|
||||
path: 'Player > Compressor >> Gain Control',
|
||||
title: 'Maximum',
|
||||
component: 'setting-text-box',
|
||||
description: '**Range:** 100 ~ 1000\n\nThe maximum allowed value for gain control. 100% is no change. 200% is double the volume.',
|
||||
process: 'to_int',
|
||||
bounds: [100, true, 1000, true]
|
||||
},
|
||||
|
||||
changed: () => this.updateGains()
|
||||
});
|
||||
}
|
||||
|
||||
this.settings.add('player.compressor.threshold', {
|
||||
default: -50,
|
||||
ui: {
|
||||
|
@ -89,13 +255,8 @@ export default class PlayerBase extends Module {
|
|||
sort: 0,
|
||||
description: '**Range:** -100 ~ 0\n\nThe decibel value above which the compression will start taking effect.',
|
||||
component: 'setting-text-box',
|
||||
process(val) {
|
||||
val = parseInt(val, 10);
|
||||
if ( isNaN(val) || ! isFinite(val) || val > 0 || val < -100 )
|
||||
return -50;
|
||||
|
||||
return val;
|
||||
}
|
||||
process: 'to_int',
|
||||
bounds: [-100, true, 0, true]
|
||||
},
|
||||
|
||||
changed: () => this.updateCompressors()
|
||||
|
@ -109,13 +270,8 @@ export default class PlayerBase extends Module {
|
|||
sort: 5,
|
||||
description: '**Range:** 0 ~ 40\n\nA decibel value representing the range above the threshold where the curve smoothly transitions to the compressed portion.',
|
||||
component: 'setting-text-box',
|
||||
process(val) {
|
||||
val = parseInt(val, 10);
|
||||
if ( isNaN(val) || ! isFinite(val) || val < 0 || val > 40 )
|
||||
return 40;
|
||||
|
||||
return val;
|
||||
}
|
||||
process: 'to_int',
|
||||
bounds: [0, true, 40, true]
|
||||
},
|
||||
|
||||
changed: () => this.updateCompressors()
|
||||
|
@ -129,13 +285,8 @@ export default class PlayerBase extends Module {
|
|||
sort: 10,
|
||||
description: '**Range:** 0 ~ 20\n\nThe amount of change, in dB, needed in the input for a 1 dB change in the output.',
|
||||
component: 'setting-text-box',
|
||||
process(val) {
|
||||
val = parseInt(val, 10);
|
||||
if ( isNaN(val) || ! isFinite(val) || val < 1 || val > 20 )
|
||||
return 12;
|
||||
|
||||
return val;
|
||||
}
|
||||
process: 'to_int',
|
||||
bounds: [0, true, 20, true]
|
||||
},
|
||||
|
||||
changed: () => this.updateCompressors()
|
||||
|
@ -149,13 +300,8 @@ export default class PlayerBase extends Module {
|
|||
sort: 15,
|
||||
description: '**Range:** 0 ~ 1\n\nThe amount of time, in seconds, required to reduce the gain by 10 dB.',
|
||||
component: 'setting-text-box',
|
||||
process(val) {
|
||||
val = parseFloat(val);
|
||||
if ( isNaN(val) || ! isFinite(val) || val < 0 || val > 1 )
|
||||
return 0;
|
||||
|
||||
return val;
|
||||
}
|
||||
process: 'to_float',
|
||||
bounds: [0, true, 1, true]
|
||||
},
|
||||
|
||||
changed: () => this.updateCompressors()
|
||||
|
@ -169,13 +315,8 @@ export default class PlayerBase extends Module {
|
|||
sort: 20,
|
||||
description: '**Range:** 0 ~ 1\nThe amount of time, in seconds, required to increase the gain by 10 dB.',
|
||||
component: 'setting-text-box',
|
||||
process(val) {
|
||||
val = parseFloat(val);
|
||||
if ( isNaN(val) || ! isFinite(val) || val < 0 || val > 1 )
|
||||
return 0.25;
|
||||
|
||||
return val;
|
||||
}
|
||||
process: 'to_float',
|
||||
bounds: [0, true, 1, true]
|
||||
},
|
||||
|
||||
changed: () => this.updateCompressors()
|
||||
|
@ -210,11 +351,7 @@ export default class PlayerBase extends Module {
|
|||
title: 'Adjust volume by scrolling with the mouse wheel.',
|
||||
description: '*This setting will not work properly on streams with visible extensions when mouse interaction with extensions is allowed.*',
|
||||
component: 'setting-select-box',
|
||||
data: [
|
||||
{value: false, title: 'Disabled'},
|
||||
{value: true, title: 'Enabled'},
|
||||
{value: 2, title: 'Enabled with Right-Click'}
|
||||
]
|
||||
data: SCROLL_OPTIONS
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -408,6 +545,7 @@ export default class PlayerBase extends Module {
|
|||
await this.settings.awaitProvider();
|
||||
await this.settings.provider.awaitReady();
|
||||
|
||||
this.css_tweaks.toggleHide('player-gain-volume', this.settings.get('player.gain.no-volume'));
|
||||
this.css_tweaks.toggle('player-volume', this.settings.get('player.volume-always-shown'));
|
||||
this.css_tweaks.toggle('player-ext-mouse', !this.settings.get('player.ext-interaction'));
|
||||
this.css_tweaks.toggle('player-hide-mouse', this.settings.get('player.hide-mouse'));
|
||||
|
@ -415,6 +553,7 @@ export default class PlayerBase extends Module {
|
|||
this.installVisibilityHook();
|
||||
this.updateHideExtensions();
|
||||
this.updateCaptionsCSS();
|
||||
this.updateShortcut();
|
||||
|
||||
this.on(':reset', this.resetAllPlayers, this);
|
||||
|
||||
|
@ -465,6 +604,32 @@ export default class PlayerBase extends Module {
|
|||
});
|
||||
}
|
||||
|
||||
updateShortcut() {
|
||||
const Mousetrap = this.Mousetrap = this.Mousetrap || this.resolve('site.web_munch')?.getModule?.('mousetrap') || window.Mousetrap;
|
||||
if ( ! Mousetrap || ! Mousetrap.bind )
|
||||
return;
|
||||
|
||||
if ( this._shortcut_bound ) {
|
||||
Mousetrap.unbind(this._shortcut_bound);
|
||||
this._shortcut_bound = null;
|
||||
}
|
||||
|
||||
const key = this.settings.get('player.compressor.shortcut');
|
||||
if ( HAS_COMPRESSOR && key && isValidShortcut(key) ) {
|
||||
Mousetrap.bind(key, this.onShortcut);
|
||||
this._shortcut_bound = key;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
onShortcut(e) {
|
||||
this.log.info('Compressor Hotkey', e);
|
||||
|
||||
for(const inst of this.Player.instances)
|
||||
this.compressPlayer(inst, e);
|
||||
}
|
||||
|
||||
|
||||
modifyPlayerClass(cls) {
|
||||
const t = this,
|
||||
old_attach = cls.prototype.maybeAttachDomEventListeners;
|
||||
|
@ -611,15 +776,19 @@ export default class PlayerBase extends Module {
|
|||
if ( player.core )
|
||||
player = player.core;
|
||||
|
||||
const state = player.state?.state;
|
||||
const state = player.state?.state,
|
||||
video = player.mediaSinkManager?.video;
|
||||
|
||||
if ( state === 'Playing' ) {
|
||||
const video = player.mediaSinkManager?.video;
|
||||
if ( video?._ffz_maybe_compress ) {
|
||||
video._ffz_maybe_compress = false;
|
||||
t.compressPlayer(this);
|
||||
}
|
||||
}
|
||||
|
||||
if ( video && video._ffz_compressed != null )
|
||||
ds.compressed = video._ffz_compressed;
|
||||
|
||||
ds.ended = state === 'Ended';
|
||||
ds.paused = state === 'Idle';
|
||||
}
|
||||
|
@ -671,7 +840,12 @@ export default class PlayerBase extends Module {
|
|||
if ( ! event )
|
||||
return;
|
||||
|
||||
if ( t.settings.get('player.volume-scroll') === 2 && event.button === 2 ) {
|
||||
const vol_scroll = t.settings.get('player.volume-scroll'),
|
||||
gain_scroll = t.settings.get('player.gain.scroll'),
|
||||
|
||||
wants_rmb = wantsRMB(vol_scroll) || wantsRMB(gain_scroll);
|
||||
|
||||
if ( wants_rmb && event.button === 2 ) {
|
||||
this.ffz_rmb = true;
|
||||
this.ffz_scrolled = false;
|
||||
}
|
||||
|
@ -699,25 +873,29 @@ export default class PlayerBase extends Module {
|
|||
}
|
||||
|
||||
cls.prototype.ffzScrollHandler = function(event) {
|
||||
const setting = t.settings.get('player.volume-scroll');
|
||||
if ( ! setting )
|
||||
return;
|
||||
const vol_scroll = t.settings.get('player.volume-scroll'),
|
||||
gain_scroll = t.settings.get('player.gain.scroll'),
|
||||
|
||||
if ( setting === 2 && ! this.ffz_rmb )
|
||||
matches_gain = gain_scroll && matchesEvent(gain_scroll, event, this.ffz_rmb),
|
||||
matches_vol = ! matches_gain && vol_scroll && matchesEvent(vol_scroll, event, this.ffz_rmb);
|
||||
|
||||
if ( ! matches_gain && ! matches_vol )
|
||||
return;
|
||||
|
||||
const delta = event.wheelDelta || -(event.deltaY || event.detail || 0),
|
||||
player = this.props?.mediaPlayerInstance,
|
||||
video = player?.mediaSinkManager?.video || player?.core?.mediaSinkManager?.video;
|
||||
|
||||
if ( ! player?.getVolume )
|
||||
if ( ! player?.getVolume || (matches_gain && ! video) )
|
||||
return;
|
||||
|
||||
if ( setting === 2 )
|
||||
if ( matches_gain ? wantsRMB(gain_scroll) : wantsRMB(vol_scroll) )
|
||||
this.ffz_scrolled = true;
|
||||
|
||||
const amount = t.settings.get('player.volume-scroll-steps'),
|
||||
old_volume = video?.volume ?? player.getVolume(),
|
||||
const amount = t.settings.get('player.volume-scroll-steps');
|
||||
|
||||
if ( matches_vol && ! (video._ffz_compressed && t.settings.get('player.gain.no-volume')) ) {
|
||||
const old_volume = video?.volume ?? player.getVolume(),
|
||||
volume = Math.max(0, Math.min(1, old_volume + (delta > 0 ? amount : -amount)));
|
||||
|
||||
player.setVolume(volume);
|
||||
|
@ -728,6 +906,28 @@ export default class PlayerBase extends Module {
|
|||
localStorage.setItem('video-muted', JSON.stringify({default: false}));
|
||||
}
|
||||
|
||||
} else if ( matches_gain ) {
|
||||
let value = video._ffz_gain_value;
|
||||
if ( value == null )
|
||||
value = t.settings.get('player.gain.default');
|
||||
|
||||
const min = t.settings.get('player.gain.min'),
|
||||
max = t.settings.get('player.gain.max');
|
||||
|
||||
if ( delta > 0 )
|
||||
value += amount;
|
||||
else
|
||||
value -= amount;
|
||||
|
||||
if ( value < min )
|
||||
value = min;
|
||||
if ( value > max )
|
||||
value = max;
|
||||
|
||||
video._ffz_gain_value = value;
|
||||
t.updateGain(this);
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
@ -902,6 +1102,7 @@ export default class PlayerBase extends Module {
|
|||
this.addPiPButton(inst);
|
||||
this.addResetButton(inst);
|
||||
this.addCompressorButton(inst, false);
|
||||
this.addGainSlider(inst, false);
|
||||
this.addMetadata(inst);
|
||||
|
||||
if ( inst._ffzUpdateVolume )
|
||||
|
@ -910,6 +1111,122 @@ export default class PlayerBase extends Module {
|
|||
this.emit(':update-gui', inst);
|
||||
}
|
||||
|
||||
addGainSlider(inst, visible_only, tries = 0) {
|
||||
const outer = inst.props.containerRef || this.fine.getChildNode(inst),
|
||||
video = inst.props.mediaPlayerInstance?.mediaSinkManager?.video || inst.props.mediaPlayerInstance?.core?.mediaSinkManager?.video,
|
||||
container = outer && outer.querySelector('.player-controls__left-control-group');
|
||||
let gain = video != null && video._ffz_compressed && video._ffz_gain;
|
||||
|
||||
if ( ! container ) {
|
||||
if ( video && ! gain )
|
||||
return;
|
||||
|
||||
if ( tries < 5 )
|
||||
return setTimeout(this.addGainSlider.bind(this, inst, visible_only, (tries || 0) + 1), 250);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const min = this.settings.get('player.gain.min'),
|
||||
max = this.settings.get('player.gain.max');
|
||||
|
||||
if ( min >= max || max <= min )
|
||||
gain = null;
|
||||
|
||||
let tip, input, extra, fill, cont = container.querySelector('.ffz--player-gain');
|
||||
if ( ! gain ) {
|
||||
if ( cont )
|
||||
cont.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! cont ) {
|
||||
const on_change = () => {
|
||||
let value = input.value / 100;
|
||||
|
||||
const min = this.settings.get('player.gain.min'),
|
||||
max = this.settings.get('player.gain.max');
|
||||
|
||||
if ( value < min )
|
||||
value = min;
|
||||
if ( value > max )
|
||||
value = max;
|
||||
|
||||
video._ffz_gain_value = value;
|
||||
gain.gain.value = value;
|
||||
|
||||
const range = max - min,
|
||||
width = (value - min) / range;
|
||||
|
||||
fill.style.width = `${width * 100}%`;
|
||||
extra.textContent = `${Math.round(value * 100)}%`;
|
||||
};
|
||||
|
||||
cont = (<div class="ffz--player-gain volume-slider__slider-container tw-relative tw-tooltip__container">
|
||||
<div class="tw-align-items-center tw-flex tw-full-height">
|
||||
<label class="tw-hide-accessible">{this.i18n.t('player.gain.label','Gain Control')}</label>
|
||||
<div class="tw-flex tw-full-width tw-relative tw-z-above">
|
||||
{input = (<input
|
||||
class="tw-range tw-range--overlay"
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
step="1"
|
||||
data-a-target="player-gain-slider"
|
||||
value="100"
|
||||
/>)}
|
||||
<div class="tw-absolute tw-border-radius-large tw-bottom-0 tw-flex tw-flex-column tw-full-width tw-justify-content-center tw-range__fill tw-range__fill--overlay tw-top-0 tw-z-below">
|
||||
<div class="tw-border-radius-large tw-range__fill-container">
|
||||
{fill = (<div
|
||||
class="tw-border-radius-large tw-range__fill-value ffz--gain-value"
|
||||
data-test-selector="tw-range__fill-value-selector"
|
||||
/>)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tw-tooltip tw-tooltip--align-center tw-tooltip--up" role="tooltip">
|
||||
<div>
|
||||
{tip = (<div class="ffz--p-tip" />)}
|
||||
{extra = (<div class="tw-regular ffz--p-value" />)}
|
||||
</div>
|
||||
</div>
|
||||
</div>);
|
||||
|
||||
/*input.addEventListener('contextmenu', e => {
|
||||
video._ffz_gain_value = null;
|
||||
this.updateGain(inst);
|
||||
e.preventDefault();
|
||||
});*/
|
||||
input.addEventListener('input', on_change);
|
||||
container.appendChild(cont);
|
||||
|
||||
} else if ( visible_only )
|
||||
return;
|
||||
else {
|
||||
input = cont.querySelector('input');
|
||||
fill = cont.querySelector('.ffz--gain-value');
|
||||
tip = cont.querySelector('.tw-tooltip .ffz--p-tip');
|
||||
extra = cont.querySelector('.tw-tooltip .ffz--p-value');
|
||||
}
|
||||
|
||||
let value = video._ffz_gain_value;
|
||||
if ( value == null )
|
||||
value = this.settings.get('player.gain.default');
|
||||
|
||||
input.min = min * 100;
|
||||
input.max = max * 100;
|
||||
input.value = value * 100;
|
||||
|
||||
const range = max - min,
|
||||
width = (value - min) / range;
|
||||
|
||||
fill.style.width = `${width * 100}%`;
|
||||
|
||||
tip.textContent = this.i18n.t('player.gain.label', 'Gain Control');
|
||||
extra.textContent = `${Math.round(value * 100)}%`;
|
||||
}
|
||||
|
||||
addCompressorButton(inst, visible_only, tries = 0) {
|
||||
const outer = inst.props.containerRef || this.fine.getChildNode(inst),
|
||||
video = inst.props.mediaPlayerInstance?.mediaSinkManager?.video || inst.props.mediaPlayerInstance?.core?.mediaSinkManager?.video,
|
||||
|
@ -967,8 +1284,8 @@ export default class PlayerBase extends Module {
|
|||
}
|
||||
|
||||
const comp_active = video._ffz_compressed,
|
||||
can_apply = this.canCompress(inst),
|
||||
label = can_apply ?
|
||||
can_apply = this.canCompress(inst);
|
||||
let label = can_apply ?
|
||||
comp_active ?
|
||||
this.i18n.t('player.comp_button.off', 'Disable Audio Compressor') :
|
||||
this.i18n.t('player.comp_button.on', 'Audio Compressor')
|
||||
|
@ -976,6 +1293,9 @@ export default class PlayerBase extends Module {
|
|||
|
||||
extra.textContent = this.i18n.t('player.comp_button.help', 'See the FFZ Control Center for details. If audio breaks, please reset the player.');
|
||||
|
||||
if ( can_apply && this._shortcut_bound )
|
||||
label = `${label} (${this._shortcut_bound})`;
|
||||
|
||||
if ( ff_el )
|
||||
ff_el.textContent += `\n${this.i18n.t('player.comp_button.firefox', 'Playback Speed controls will not function for Firefox users when the Compressor has been enabled.')}`;
|
||||
|
||||
|
@ -988,11 +1308,19 @@ export default class PlayerBase extends Module {
|
|||
}
|
||||
|
||||
compressPlayer(inst, e) {
|
||||
const video = inst.props.mediaPlayerInstance?.mediaSinkManager?.video ||
|
||||
inst.props.mediaPlayerInstance?.core?.mediaSinkManager?.video;
|
||||
const player = inst.props.mediaPlayerInstance,
|
||||
core = player.core || player,
|
||||
video = core?.mediaSinkManager?.video;
|
||||
|
||||
if ( ! video || ! HAS_COMPRESSOR )
|
||||
return;
|
||||
|
||||
// Backup the setVolume method.
|
||||
if ( ! core._ffz_setVolume ) {
|
||||
core._ffz_setVolume = core.setVolume;
|
||||
core._ffz_fakeVolume = () => {};
|
||||
}
|
||||
|
||||
video._ffz_maybe_compress = false;
|
||||
const compressed = video._ffz_compressed || false;
|
||||
let wanted = video._ffz_toggled ? video._ffz_state : this.settings.get('player.compressor.default');
|
||||
|
@ -1015,7 +1343,11 @@ export default class PlayerBase extends Module {
|
|||
return;
|
||||
}
|
||||
|
||||
if ( wanted == compressed || (e == null && video._ffz_toggled) )
|
||||
let gain = video._ffz_gain;
|
||||
const want_gain = HAS_GAIN && this.settings.get('player.gain.enable'),
|
||||
has_gain = gain != null;
|
||||
|
||||
if ( ((wanted == compressed) || (e == null && video._ffz_toggled)) && has_gain == want_gain )
|
||||
return;
|
||||
|
||||
const ctx = video._ffz_context,
|
||||
|
@ -1025,18 +1357,88 @@ export default class PlayerBase extends Module {
|
|||
if ( ! ctx || ! comp || ! src )
|
||||
return;
|
||||
|
||||
if ( want_gain && ! gain ) {
|
||||
gain = video._ffz_gain = ctx.createGain();
|
||||
let value = video._ffz_gain_value;
|
||||
if ( value == null )
|
||||
value = this.settings.get('player.gain.default');
|
||||
|
||||
gain.gain.value = value;
|
||||
comp.connect(gain);
|
||||
|
||||
if ( compressed ) {
|
||||
comp.disconnect(ctx.destination);
|
||||
gain.connect(ctx.destination);
|
||||
}
|
||||
|
||||
} else if ( ! want_gain && gain ) {
|
||||
comp.disconnect(gain);
|
||||
if ( compressed ) {
|
||||
gain.disconnect(ctx.destination);
|
||||
comp.connect(ctx.destination);
|
||||
}
|
||||
|
||||
gain = video._ffz_gain = null;
|
||||
}
|
||||
|
||||
if ( wanted != compressed ) {
|
||||
if ( wanted ) {
|
||||
src.disconnect(ctx.destination);
|
||||
src.connect(comp);
|
||||
if ( gain ) {
|
||||
gain.connect(ctx.destination);
|
||||
if ( this.settings.get('player.gain.no-volume') ) {
|
||||
video._ffz_pregain_volume = core.getVolume();
|
||||
core._ffz_setVolume(1);
|
||||
core.setVolume = core._ffz_fakeVolume;
|
||||
}
|
||||
|
||||
} else
|
||||
comp.connect(ctx.destination);
|
||||
} else {
|
||||
src.disconnect(comp);
|
||||
if ( gain ) {
|
||||
gain.disconnect(ctx.destination);
|
||||
if ( video._ffz_pregain_volume != null ) {
|
||||
core._ffz_setVolume(video._ffz_pregain_volume);
|
||||
core.setVolume = core._ffz_setVolume;
|
||||
video._ffz_pregain_volume = null;
|
||||
}
|
||||
|
||||
} else
|
||||
comp.disconnect(ctx.destination);
|
||||
src.connect(ctx.destination);
|
||||
}
|
||||
}
|
||||
|
||||
if ( inst.props.containerRef )
|
||||
inst.props.containerRef.dataset.compressed = wanted;
|
||||
|
||||
video._ffz_compressed = wanted;
|
||||
this.addCompressorButton(inst);
|
||||
this.addGainSlider(inst);
|
||||
}
|
||||
|
||||
updateGainVolume(inst) {
|
||||
const player = inst.props.mediaPlayerInstance,
|
||||
core = player.core || player,
|
||||
video = core?.mediaSinkManager?.video;
|
||||
|
||||
if ( ! video || ! video._ffz_compressed )
|
||||
return;
|
||||
|
||||
const setting = this.settings.get('player.gain.no-volume');
|
||||
|
||||
if ( setting && video._ffz_pregain_volume == null ) {
|
||||
video._ffz_pregain_volume = core.getVolume();
|
||||
core._ffz_setVolume(1);
|
||||
core.setVolume = core._ffz_fakeVolume;
|
||||
|
||||
} else if ( ! setting && video._ffz_pregain_volume != null ) {
|
||||
core._ffz_setVolume(video._ffz_pregain_volume);
|
||||
core.setVolume = core._ffz_setVolume;
|
||||
video._ffz_pregain_volume = null;
|
||||
}
|
||||
}
|
||||
|
||||
canCompress(inst) { // eslint-disable-line class-methods-use-this
|
||||
|
@ -1079,12 +1481,47 @@ export default class PlayerBase extends Module {
|
|||
src.connect(ctx.destination);
|
||||
|
||||
comp = video._ffz_compressor = ctx.createDynamicsCompressor();
|
||||
|
||||
if ( this.settings.get('player.gain.enable') ) {
|
||||
const gain = video._ffz_gain = ctx.createGain();
|
||||
let value = video._ffz_gain_value;
|
||||
if ( value == null )
|
||||
value = this.settings.get('player.gain.default');
|
||||
gain.gain.value = value;
|
||||
comp.connect(gain);
|
||||
}
|
||||
|
||||
video._ffz_compressed = false;
|
||||
}
|
||||
|
||||
this.updateCompressor(null, comp);
|
||||
}
|
||||
|
||||
updateGains() {
|
||||
for(const inst of this.Player.instances)
|
||||
this.updateGain(inst);
|
||||
}
|
||||
|
||||
updateGain(inst, gain, video, update_gui = true) {
|
||||
if ( ! video )
|
||||
video = inst.props.mediaPlayerInstance?.mediaSinkManager?.video ||
|
||||
inst.props.mediaPlayerInstance?.core?.mediaSinkManager?.video;
|
||||
|
||||
if ( gain == null )
|
||||
gain = video?._ffz_gain;
|
||||
|
||||
if ( ! video || ! gain )
|
||||
return;
|
||||
|
||||
let value = video._ffz_gain_value;
|
||||
if ( value == null )
|
||||
value = this.settings.get('player.gain.default');
|
||||
|
||||
gain.gain.value = value;
|
||||
if ( update_gui )
|
||||
this.addGainSlider(inst);
|
||||
}
|
||||
|
||||
updateCompressors() {
|
||||
for(const inst of this.Player.instances)
|
||||
this.updateCompressor(inst);
|
||||
|
@ -1415,20 +1852,26 @@ export default class PlayerBase extends Module {
|
|||
const duration = player.getDuration?.() ?? Infinity;
|
||||
let position = -1;
|
||||
|
||||
const core = player.core || player;
|
||||
if ( core._ffz_setVolume )
|
||||
core.setVolume = core._ffz_setVolume;
|
||||
|
||||
if ( isFinite(duration) && ! isNaN(duration) && duration > 0 )
|
||||
position = player.getPosition();
|
||||
|
||||
const video = player.mediaSinkManager?.video || player.core?.mediaSinkManager?.video;
|
||||
if ( video?._ffz_compressor && player.attachHTMLVideoElement ) {
|
||||
const new_vid = createElement('video'),
|
||||
vol = video?.volume ?? player.getVolume(),
|
||||
vol = video?._ffz_pregain_volume ?? video?.volume ?? player.getVolume(),
|
||||
muted = player.isMuted();
|
||||
|
||||
new_vid._ffz_gain_value = video._ffz_gain_value;
|
||||
new_vid._ffz_state = video._ffz_state;
|
||||
new_vid._ffz_toggled = video._ffz_toggled;
|
||||
new_vid._ffz_maybe_compress = true;
|
||||
new_vid.volume = muted ? 0 : vol;
|
||||
new_vid.playsInline = true;
|
||||
|
||||
this.installPlaybackRate(new_vid);
|
||||
video.replaceWith(new_vid);
|
||||
player.attachHTMLVideoElement(new_vid);
|
||||
|
|
|
@ -513,7 +513,7 @@ export default class Channel extends Module {
|
|||
while(node != null && channel == null && j < 10) {
|
||||
let state = node?.memoizedState;
|
||||
i=0;
|
||||
while(state != null && channel == null && i < 25) {
|
||||
while(state != null && channel == null && i < 50) {
|
||||
state = state?.next;
|
||||
channel = state?.memoizedState?.current?.previousData?.result?.data?.userOrError;
|
||||
i++;
|
||||
|
|
|
@ -28,6 +28,8 @@ const CLASSES = {
|
|||
|
||||
'prime-offers': '.top-nav__prime',
|
||||
|
||||
'player-gain-volume': '.video-player__overlay[data-compressed="true"] .volume-slider__slider-container:not(.ffz--player-gain)',
|
||||
|
||||
'player-ext': '.video-player .extension-taskbar,.video-player .extension-container,.video-player .extensions-dock__layout,.video-player .extensions-notifications,.video-player .extensions-video-overlay-size-container,.video-player .extensions-dock__layout',
|
||||
'player-ext-hover': '.video-player__overlay[data-controls="false"] .extension-taskbar,.video-player__overlay[data-controls="false"] .extension-container,.video-player__overlay[data-controls="false"] .extensions-dock__layout,.video-player__overlay[data-controls="false"] .extensions-notifications,.video-player__overlay[data-controls="false"] .extensions-video-overlay-size-container',
|
||||
|
||||
|
|
|
@ -19,6 +19,20 @@ export const LV_SERVER = 'https://cbenni.com/api';
|
|||
export const LV_SOCKET_SERVER = 'wss://cbenni.com/socket.io/';
|
||||
|
||||
|
||||
export const BAD_HOTKEYS = [
|
||||
'f',
|
||||
'space',
|
||||
'k',
|
||||
'shift+up',
|
||||
'shift+down',
|
||||
'esc',
|
||||
'm',
|
||||
'?',
|
||||
'alt+t',
|
||||
'alt+x'
|
||||
];
|
||||
|
||||
|
||||
export const KEYS = {
|
||||
Tab: 9,
|
||||
Enter: 13,
|
||||
|
|
|
@ -1,7 +1,17 @@
|
|||
'use strict';
|
||||
|
||||
import {BAD_HOTKEYS} from 'utilities/constants';
|
||||
|
||||
const HOP = Object.prototype.hasOwnProperty;
|
||||
|
||||
export function isValidShortcut(key) {
|
||||
if ( ! key )
|
||||
return false;
|
||||
|
||||
key = key.toLowerCase().trim();
|
||||
return ! BAD_HOTKEYS.includes(key);
|
||||
}
|
||||
|
||||
// Source: https://gist.github.com/jed/982883 (WTFPL)
|
||||
export function generateUUID(input) {
|
||||
return input // if the placeholder was passed, return
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue