mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-08-02 16:08:31 +00:00
4.20.62
The Embedded Player Update! Adds support for the Compressor, Reset Button, and metadata to the embedded / pop-out player using a minimal build of FFZ. * Added: Support for the embedded player. * Fixed: Update the regex for clip URLs to match new extended slugs. * Fixed: Bugs in `experiments` and `raven` when the `site` module cannot be loaded or does not provide `getCore()`. * Fixed: Bug in the main menu where the menu may sometimes become unresponsive when opening to a page with no settings. * Fixed: Attempting to initialize the Compressor when a user has not yet interacted with a page may result in a broken audio stack. * Fixed: Setting Profile filters that are unknown would always match, rather than always failing. This is problematic if an add-on that provides a filter type has not yet loaded.
This commit is contained in:
parent
264c375f13
commit
a473c6eb93
23 changed files with 2362 additions and 49 deletions
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "frankerfacez",
|
||||
"author": "Dan Salvato LLC",
|
||||
"version": "4.20.61",
|
||||
"version": "4.20.62",
|
||||
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
|
|
|
@ -2,11 +2,13 @@
|
|||
'use strict';
|
||||
(() => {
|
||||
// Don't run on certain sub-domains.
|
||||
if ( /^(?:localhost\.rig|blog|player|im|chatdepot|tmi|api|brand|dev)\./.test(location.hostname) )
|
||||
if ( /^(?:localhost\.rig|blog|im|chatdepot|tmi|api|brand|dev)\./.test(location.hostname) )
|
||||
return;
|
||||
|
||||
const DEBUG = localStorage.ffzDebugMode == 'true' && document.body.classList.contains('ffz-dev') && ! window.Ember,
|
||||
FLAVOR = location.pathname === '/p/ffz_bridge/' ? 'bridge' : 'avalon',
|
||||
const DEBUG = localStorage.ffzDebugMode == 'true' && document.body.classList.contains('ffz-dev'),
|
||||
FLAVOR =
|
||||
location.hostname.includes('player') ? 'player' :
|
||||
(location.pathname === '/p/ffz_bridge/' ? 'bridge' : 'avalon'),
|
||||
SERVER = DEBUG ? '//localhost:8000' : '//cdn.frankerfacez.com',
|
||||
CLIPS = /clips\.twitch\.tv/.test(location.hostname) ? 'clips/' : '',
|
||||
|
||||
|
|
|
@ -261,7 +261,7 @@ export default class ExperimentManager extends Module {
|
|||
// Twitch Experiments
|
||||
|
||||
getTwitchType(type) {
|
||||
const core = this.resolve('site')?.getCore();
|
||||
const core = this.resolve('site')?.getCore?.();
|
||||
if ( core?.experiments?.getExperimentType )
|
||||
return core.experiments.getExperimentType(type);
|
||||
|
||||
|
@ -275,7 +275,7 @@ export default class ExperimentManager extends Module {
|
|||
}
|
||||
|
||||
getTwitchTypeByKey(key) {
|
||||
const core = this.resolve('site')?.getCore(),
|
||||
const core = this.resolve('site')?.getCore?.(),
|
||||
exps = core && core.experiments,
|
||||
exp = exps?.experiments?.[key];
|
||||
|
||||
|
@ -289,13 +289,13 @@ export default class ExperimentManager extends Module {
|
|||
if ( window.__twilightSettings )
|
||||
return window.__twilightSettings.experiments;
|
||||
|
||||
const core = this.resolve('site')?.getCore();
|
||||
const core = this.resolve('site')?.getCore?.();
|
||||
return core && core.experiments.experiments;
|
||||
}
|
||||
|
||||
|
||||
usingTwitchExperiment(key) {
|
||||
const core = this.resolve('site')?.getCore();
|
||||
const core = this.resolve('site')?.getCore?.();
|
||||
return core && has(core.experiments.assignments, key)
|
||||
}
|
||||
|
||||
|
@ -305,7 +305,7 @@ export default class ExperimentManager extends Module {
|
|||
overrides[key] = value;
|
||||
Cookie.set(OVERRIDE_COOKIE, overrides, COOKIE_OPTIONS);
|
||||
|
||||
const core = this.resolve('site')?.getCore();
|
||||
const core = this.resolve('site')?.getCore?.();
|
||||
if ( core )
|
||||
core.experiments.overrides[key] = value;
|
||||
|
||||
|
@ -321,7 +321,7 @@ export default class ExperimentManager extends Module {
|
|||
delete overrides[key];
|
||||
Cookie.set(OVERRIDE_COOKIE, overrides, COOKIE_OPTIONS);
|
||||
|
||||
const core = this.resolve('site')?.getCore();
|
||||
const core = this.resolve('site')?.getCore?.();
|
||||
if ( core )
|
||||
delete core.experiments.overrides[key];
|
||||
|
||||
|
@ -334,7 +334,7 @@ export default class ExperimentManager extends Module {
|
|||
}
|
||||
|
||||
getTwitchAssignment(key, channel = null) {
|
||||
const core = this.resolve('site')?.getCore(),
|
||||
const core = this.resolve('site')?.getCore?.(),
|
||||
exps = core && core.experiments;
|
||||
|
||||
if ( ! exps )
|
||||
|
@ -378,7 +378,7 @@ export default class ExperimentManager extends Module {
|
|||
}
|
||||
|
||||
_rebuildTwitchKey(key, is_set, new_val) {
|
||||
const core = this.resolve('site')?.getCore(),
|
||||
const core = this.resolve('site')?.getCore?.(),
|
||||
exps = core.experiments,
|
||||
|
||||
old_val = has(exps.assignments, key) ?
|
||||
|
|
|
@ -4,8 +4,10 @@
|
|||
// Rich Content Providers
|
||||
// ============================================================================
|
||||
|
||||
const CLIP_URL = /^(?:https?:\/\/)?clips\.twitch\.tv\/(\w+)(?:\/)?(\w+)?(?:\/edit)?/;
|
||||
const NEW_CLIP_URL = /^(?:https?:\/\/)?(?:www\.)?twitch\.tv\/\w+\/clip\/(\w+)/;
|
||||
//const CLIP_URL = /^(?:https?:\/\/)?clips\.twitch\.tv\/(\w+)(?:\/)?(\w+)?(?:\/edit)?/;
|
||||
//const NEW_CLIP_URL = /^(?:https?:\/\/)?(?:www\.)?twitch\.tv\/\w+\/clip\/(\w+)/;
|
||||
const CLIP_URL = /^(?:https?:\/\/)?clips\.twitch\.tv\/([a-z0-9-]+)(?:\/)?(\w+)?(?:\/edit)?/i;
|
||||
const NEW_CLIP_URL = /^(?:https?:\/\/)?(?:www\.)?twitch\.tv\/\w+\/clip\/([a-z0-9-]+)/i;
|
||||
const VIDEO_URL = /^(?:https?:\/\/)?(?:www\.)?twitch\.tv\/(?:\w+\/v|videos)\/(\w+)/;
|
||||
const USER_URL = /^(?:https?:\/\/)?(?:www\.)?twitch\.tv\/([^/]+)$/;
|
||||
|
||||
|
@ -258,6 +260,7 @@ export const Clips = {
|
|||
|
||||
return {
|
||||
url: token.url,
|
||||
accent: '#6441a4',
|
||||
|
||||
short: {
|
||||
type: 'header',
|
||||
|
|
|
@ -117,12 +117,13 @@ export default {
|
|||
},
|
||||
|
||||
onBeforeChange(current, new_item) {
|
||||
for(const child of this.$refs.children)
|
||||
if ( child && child.onBeforeChange ) {
|
||||
const res = child.onBeforeChange(current, new_item);
|
||||
if ( res !== undefined )
|
||||
return res;
|
||||
}
|
||||
if ( this.$refs.children )
|
||||
for(const child of this.$refs.children)
|
||||
if ( child && child.onBeforeChange ) {
|
||||
const res = child.onBeforeChange(current, new_item);
|
||||
if ( res !== undefined )
|
||||
return res;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
'use strict';
|
||||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Channel Metadata
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
'use strict';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import RavenLogger from './raven';
|
||||
|
||||
import Logger from 'utilities/logging';
|
||||
import Module from 'utilities/module';
|
||||
import { timeout } from 'utilities/object';
|
||||
|
||||
import {DEBUG} from 'utilities/constants';
|
||||
import {timeout} from 'utilities/object';
|
||||
|
||||
import SettingsManager from './settings/index';
|
||||
import ExperimentManager from './experiments';
|
||||
import {TranslationManager} from './i18n';
|
||||
import Site from './sites/player';
|
||||
|
||||
class FFZPlayer extends Module {
|
||||
constructor() {
|
||||
|
@ -46,7 +48,7 @@ class FFZPlayer extends Module {
|
|||
this.inject('settings', SettingsManager);
|
||||
this.inject('experiments', ExperimentManager);
|
||||
this.inject('i18n', TranslationManager);
|
||||
|
||||
this.inject('site', Site);
|
||||
|
||||
// ========================================================================
|
||||
// Startup
|
||||
|
@ -106,11 +108,6 @@ class FFZPlayer extends Module {
|
|||
-------------------------------------------------------------------------------
|
||||
${typeof x[1] === 'string' ? x[1] : JSON.stringify(x[1], null, 4)}`).join('\n\n');
|
||||
}
|
||||
|
||||
async onEnable() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -127,11 +124,11 @@ const VER = FFZPlayer.version_info = {
|
|||
`${VER.major}.${VER.minor}.${VER.revision}${VER.extra || ''}${DEBUG ? '-dev' : ''}`
|
||||
}
|
||||
|
||||
FFZPlayer.utilities = {
|
||||
// We don't support addons in the player right now, so
|
||||
/*FFZPlayer.utilities = {
|
||||
addon: require('utilities/addon'),
|
||||
color: require('utilities/color'),
|
||||
constants: require('utilities/constants'),
|
||||
dialog: require('utilities/dialog'),
|
||||
dom: require('utilities/dom'),
|
||||
events: require('utilities/events'),
|
||||
fontAwesome: require('utilities/font-awesome'),
|
||||
|
@ -144,8 +141,8 @@ FFZPlayer.utilities = {
|
|||
i18n: require('utilities/translation-core'),
|
||||
dayjs: require('dayjs'),
|
||||
popper: require('popper.js').default
|
||||
}
|
||||
}*/
|
||||
|
||||
|
||||
window.FFZPlayer = FFZPlayer;
|
||||
window.ffz_player = new FFZPlayer();
|
||||
window.ffz = new FFZPlayer();
|
|
@ -131,7 +131,7 @@ export default class RavenLogger extends Module {
|
|||
if ( munch )
|
||||
munch.getRequire().then(() => {
|
||||
const site = this.resolve('site'),
|
||||
core = site.getCore(),
|
||||
core = site?.getCore?.(),
|
||||
logger = core?.logger;
|
||||
|
||||
if ( logger && ! logger.rootLogger ) {
|
||||
|
@ -151,7 +151,7 @@ export default class RavenLogger extends Module {
|
|||
autoBreadcrumbs: {
|
||||
console: false
|
||||
},
|
||||
release: (window.FrankerFaceZ || window.FFZBridge).version_info.toString(),
|
||||
release: (window.FrankerFaceZ || window.FFZPlayer || window.FFZBridge).version_info.toString(),
|
||||
environment: DEBUG ? 'development' : 'production',
|
||||
captureUnhandledRejections: false,
|
||||
ignoreErrors: [
|
||||
|
@ -380,7 +380,7 @@ export default class RavenLogger extends Module {
|
|||
|
||||
|
||||
buildTags() {
|
||||
const core = this.resolve('site')?.getCore(),
|
||||
const core = this.resolve('site')?.getCore?.(),
|
||||
out = {};
|
||||
|
||||
out.flavor = this.site?.constructor.name;
|
||||
|
|
|
@ -86,7 +86,7 @@ export default {
|
|||
}
|
||||
|
||||
try {
|
||||
return decodeURI(new URL(this.route.url(parts), location));
|
||||
return decodeURI(new URL(this.route.url(parts), this.route.domain ? `https://${this.route.domain}` : location));
|
||||
} catch(err) {
|
||||
return '(unable to render url)';
|
||||
}
|
||||
|
|
|
@ -15,12 +15,6 @@ export default class BaseSite extends Module {
|
|||
this.log.info(`Using: ${this.constructor.name}`);
|
||||
}
|
||||
|
||||
async populateModules() {
|
||||
const ctx = await require.context('site/modules', true, /(?:^(?:\.\/)?[^/]+|index)\.jsx?$/);
|
||||
const modules = await this.populate(ctx, this.log);
|
||||
this.log.info(`Loaded descriptions of ${Object.keys(modules).length} modules.`);
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// DOM Manipulation
|
||||
|
|
87
src/sites/player/css_tweaks/index.js
Normal file
87
src/sites/player/css_tweaks/index.js
Normal file
|
@ -0,0 +1,87 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// CSS Tweaks for Twitch Twilight
|
||||
// ============================================================================
|
||||
|
||||
import Module from 'utilities/module';
|
||||
import {ManagedStyle} from 'utilities/dom';
|
||||
import {has} from 'utilities/object';
|
||||
|
||||
|
||||
const CLASSES = {
|
||||
'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',
|
||||
};
|
||||
|
||||
|
||||
export default class CSSTweaks extends Module {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.should_enable = true;
|
||||
|
||||
this.inject('settings');
|
||||
|
||||
this.style = new ManagedStyle;
|
||||
this.chunks = {};
|
||||
this.chunks_loaded = false;
|
||||
}
|
||||
|
||||
|
||||
toggleHide(key, val) {
|
||||
const k = `hide--${key}`;
|
||||
if ( ! val ) {
|
||||
this.style.delete(k);
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! has(CLASSES, key) )
|
||||
throw new Error(`cannot find class for "${key}"`);
|
||||
|
||||
this.style.set(k, `${CLASSES[key]} { display: none !important }`);
|
||||
}
|
||||
|
||||
|
||||
async toggle(key, val) {
|
||||
if ( ! val ) {
|
||||
this.style.delete(key);
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! this.chunks_loaded )
|
||||
await this.populate();
|
||||
|
||||
if ( ! has(this.chunks, key) )
|
||||
throw new Error(`cannot find chunk "${key}"`);
|
||||
|
||||
this.style.set(key, this.chunks[key]);
|
||||
}
|
||||
|
||||
|
||||
set(key, val) { return this.style.set(key, val) }
|
||||
delete(key) { return this.style.delete(key) }
|
||||
|
||||
setVariable(key, val, scope = 'body') {
|
||||
this.style.set(`var--${key}`, `${scope} { --ffz-${key}: ${val}; }`);
|
||||
}
|
||||
|
||||
deleteVariable(key) { this.style.delete(`var--${key}`) }
|
||||
|
||||
|
||||
populate() {
|
||||
if ( this.chunks_loaded )
|
||||
return;
|
||||
|
||||
return new Promise(async r => {
|
||||
const raw = (await import(/* webpackChunkName: "player-css-tweaks" */ './styles.js')).default;
|
||||
for(const key of raw.keys()) {
|
||||
const k = key.slice(2, key.length - (key.endsWith('.scss') ? 5 : 4));
|
||||
this.chunks[k] = raw(key).default;
|
||||
}
|
||||
|
||||
this.chunks_loaded = true;
|
||||
r();
|
||||
})
|
||||
}
|
||||
}
|
3
src/sites/player/css_tweaks/styles.js
Normal file
3
src/sites/player/css_tweaks/styles.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
'use strict';
|
||||
|
||||
export default require.context('!raw-loader!sass-loader!./styles', false, /\.s?css$/);
|
5
src/sites/player/css_tweaks/styles/player-ext-mouse.scss
Normal file
5
src/sites/player/css_tweaks/styles/player-ext-mouse.scss
Normal file
|
@ -0,0 +1,5 @@
|
|||
.video-player .extension-overlay__iframe,
|
||||
.video-player .extension-overlay,
|
||||
.video-player .extension-view__iframe {
|
||||
pointer-events: none !important;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
.video-player__overlay[data-controls="false"][data-paused="false"][data-ended="false"] {
|
||||
cursor: none;
|
||||
}
|
3
src/sites/player/css_tweaks/styles/player-volume.scss
Normal file
3
src/sites/player/css_tweaks/styles/player-volume.scss
Normal file
|
@ -0,0 +1,3 @@
|
|||
.video-player .volume-slider__slider-container {
|
||||
opacity: 1 !important;
|
||||
}
|
157
src/sites/player/index.jsx
Normal file
157
src/sites/player/index.jsx
Normal file
|
@ -0,0 +1,157 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Standalone Player
|
||||
// ============================================================================
|
||||
|
||||
import {createElement} from 'utilities/dom';
|
||||
|
||||
import BaseSite from '../base';
|
||||
|
||||
import Fine from 'utilities/compat/fine';
|
||||
import Player from './player';
|
||||
import CSSTweaks from './css_tweaks';
|
||||
import Tooltips from 'src/modules/tooltips';
|
||||
|
||||
import MAIN_URL from './styles/player-main.scss';
|
||||
|
||||
// ============================================================================
|
||||
// The Site
|
||||
// ============================================================================
|
||||
|
||||
export default class PlayerSite extends BaseSite {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.inject('i18n');
|
||||
this.inject(Fine);
|
||||
this.inject(Player);
|
||||
this.inject('tooltips', Tooltips);
|
||||
this.inject('css_tweaks', CSSTweaks);
|
||||
|
||||
this.DataSource = this.fine.define(
|
||||
'data-source',
|
||||
n => n.consentMetadata && n.onPlaying && n.props && n.props.data
|
||||
);
|
||||
|
||||
this.PlayerMenu = this.fine.define(
|
||||
'player-menu',
|
||||
n => n.closeSettingsMenu && n.state && n.state.activeMenu && n.getMaxMenuHeight
|
||||
);
|
||||
}
|
||||
|
||||
onEnable() {
|
||||
this.settings = this.resolve('settings');
|
||||
|
||||
this.DataSource.on('mount', this.updateData, this);
|
||||
this.DataSource.on('update', this.updateData, this);
|
||||
this.DataSource.ready((cls, instances) => {
|
||||
for(const inst of instances)
|
||||
this.updateData(inst);
|
||||
});
|
||||
|
||||
this.PlayerMenu.on('mount', this.updateMenu, this);
|
||||
this.PlayerMenu.on('update', this.updateMenu, this);
|
||||
this.PlayerMenu.ready((cls, instances) => {
|
||||
for(const inst of instances)
|
||||
this.updateMenu(inst);
|
||||
});
|
||||
|
||||
this.on('i18n:update', () => {
|
||||
for(const inst of this.PlayerMenu.instances)
|
||||
this.updateMenu(inst);
|
||||
});
|
||||
|
||||
// Window Size
|
||||
const update_size = () => this.settings.updateContext({
|
||||
size: {
|
||||
height: window.innerHeight,
|
||||
width: window.innerWidth
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('resize', update_size);
|
||||
update_size();
|
||||
|
||||
this.settings.updateContext({
|
||||
route: {
|
||||
name: 'popout-player',
|
||||
parts: ['/'],
|
||||
domain: 'player.twitch.tv'
|
||||
},
|
||||
route_data: ['/']
|
||||
});
|
||||
|
||||
document.head.appendChild(createElement('link', {
|
||||
href: MAIN_URL,
|
||||
rel: 'stylesheet',
|
||||
type: 'text/css',
|
||||
crossOrigin: 'anonymous'
|
||||
}));
|
||||
}
|
||||
|
||||
get data() {
|
||||
return this.DataSource.first;
|
||||
}
|
||||
|
||||
updateData(inst) {
|
||||
const user = inst?.props?.data?.user,
|
||||
|
||||
bcast = user?.broadcastSettings,
|
||||
game = user?.stream?.game;
|
||||
|
||||
this.settings.updateContext({
|
||||
title: bcast?.title,
|
||||
channelID: user?.id,
|
||||
category: game?.name,
|
||||
categoryID: game?.id
|
||||
});
|
||||
}
|
||||
|
||||
updateMenu(inst) {
|
||||
const outer = this.fine.getChildNode(inst),
|
||||
container = outer && outer.querySelector('div[data-a-target="player-settings-menu"]');
|
||||
|
||||
if ( ! container )
|
||||
return;
|
||||
|
||||
const should_render = inst.state.activeMenu === 'settings-menu__main';
|
||||
|
||||
let lbl, cont = container.querySelector('.ffz--cc-button');
|
||||
if ( ! cont ) {
|
||||
if ( ! should_render )
|
||||
return;
|
||||
|
||||
const handler = () => {
|
||||
const win = window.open(
|
||||
'https://twitch.tv/popout/frankerfacez/chat?ffz-settings=player',
|
||||
'_blank',
|
||||
'resizable=yes,scrollbars=yes,width=850,height=600'
|
||||
);
|
||||
|
||||
if ( win )
|
||||
win.focus();
|
||||
}
|
||||
|
||||
cont = (<div class="tw-mg-t-1 tw-border-t tw-pd-t-1 tw-full-width tw-relative ffz--cc-button">
|
||||
<button
|
||||
class="tw-block tw-border-radius-medium tw-full-width ffz-interactable ffz-interactable--hover-enabled ffz-interactable--default tw-interactive"
|
||||
onclick={handler}
|
||||
>
|
||||
<div class="tw-align-items-center tw-flex tw-pd-05 tw-relative">
|
||||
{lbl = <div class="tw-flex-grow-1" />}
|
||||
</div>
|
||||
</button>
|
||||
</div>);
|
||||
|
||||
container.appendChild(cont);
|
||||
|
||||
} else if ( ! should_render ) {
|
||||
cont.remove();
|
||||
return;
|
||||
} else
|
||||
lbl = cont.querySelector('button > div > div');
|
||||
|
||||
lbl.textContent = this.i18n.t('site.menu_button', 'FrankerFaceZ Control Center');
|
||||
}
|
||||
}
|
667
src/sites/player/metadata.jsx
Normal file
667
src/sites/player/metadata.jsx
Normal file
|
@ -0,0 +1,667 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Player Metadata
|
||||
// ============================================================================
|
||||
|
||||
import {createElement, ClickOutside, setChildren} from 'utilities/dom';
|
||||
import {maybe_call} from 'utilities/object';
|
||||
|
||||
import {duration_to_string, durationForURL} from 'utilities/time';
|
||||
|
||||
import Module from 'utilities/module';
|
||||
import Tooltip from 'utilities/tooltip';
|
||||
|
||||
export default class Metadata extends Module {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.inject('settings');
|
||||
this.inject('i18n');
|
||||
this.inject('site.tooltips');
|
||||
|
||||
this.definitions = {};
|
||||
|
||||
this.settings.add('metadata.player-stats', {
|
||||
default: false,
|
||||
changed: () => this.updateMetadata('player-stats')
|
||||
});
|
||||
|
||||
this.settings.add('metadata.stream-delay-warning', {
|
||||
default: 0
|
||||
});
|
||||
|
||||
this.settings.add('metadata.uptime', {
|
||||
default: 1,
|
||||
changed: () => this.updateMetadata('uptime')
|
||||
});
|
||||
|
||||
this.definitions.uptime = {
|
||||
inherit: true,
|
||||
no_arrow: true,
|
||||
|
||||
refresh() { return this.settings.get('metadata.uptime') > 0 },
|
||||
|
||||
setup(data) {
|
||||
let created = data?.channel?.live_since;
|
||||
if ( ! created )
|
||||
return {};
|
||||
|
||||
if ( !(created instanceof Date) )
|
||||
created = new Date(created);
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
return {
|
||||
created,
|
||||
uptime: created ? Math.floor((now - created.getTime()) / 1000) : -1,
|
||||
getBroadcastID: data.getBroadcastID
|
||||
}
|
||||
},
|
||||
|
||||
order: 2,
|
||||
icon: 'ffz-i-clock',
|
||||
|
||||
label(data) {
|
||||
const setting = this.settings.get('metadata.uptime');
|
||||
if ( ! setting || ! data.created )
|
||||
return null;
|
||||
|
||||
return duration_to_string(data.uptime, false, false, false, setting !== 2);
|
||||
},
|
||||
|
||||
subtitle: () => this.i18n.t('metadata.uptime.subtitle', 'Uptime'),
|
||||
|
||||
tooltip(data) {
|
||||
if ( ! data.created )
|
||||
return null;
|
||||
|
||||
return [
|
||||
this.i18n.t(
|
||||
'metadata.uptime.tooltip',
|
||||
'Stream Uptime'
|
||||
),
|
||||
<div class="tw-pd-t-05">
|
||||
{this.i18n.t(
|
||||
'metadata.uptime.since',
|
||||
'(since {since,datetime})',
|
||||
{since: data.created}
|
||||
)}
|
||||
</div>
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
this.definitions['player-stats'] = {
|
||||
button: true,
|
||||
inherit: true,
|
||||
modview: true,
|
||||
|
||||
refresh() {
|
||||
return this.settings.get('metadata.player-stats')
|
||||
},
|
||||
|
||||
setup() {
|
||||
const Player = this.resolve('site.player'),
|
||||
player = Player.current;
|
||||
|
||||
let stats;
|
||||
|
||||
if ( ! player )
|
||||
stats = null;
|
||||
|
||||
else if ( typeof player.getPlaybackStats === 'function' ) {
|
||||
stats = player.getPlaybackStats();
|
||||
|
||||
} else if ( typeof player.getVideoInfo === 'function' ) {
|
||||
const temp = player.getVideoInfo();
|
||||
stats = {
|
||||
backendVersion: maybe_call(player.getVersion, player),
|
||||
bufferSize: temp.video_buffer_size,
|
||||
displayResolution: `${temp.vid_display_width}x${temp.vid_display_height}`,
|
||||
fps: temp.current_fps,
|
||||
hlsLatencyBroadcaster: temp.hls_latency_broadcaster / 1000,
|
||||
hlsLatencyEncoder: temp.hls_latency_encoder / 1000,
|
||||
memoryUsage: `${temp.totalMemoryNumber} MB`,
|
||||
rate: maybe_call(player.getPlaybackRate, player) || 1,
|
||||
playbackRate: temp.current_bitrate,
|
||||
skippedFrames: temp.dropped_frames,
|
||||
videoResolution: `${temp.vid_width}x${temp.vid_height}`
|
||||
}
|
||||
} else {
|
||||
const videoHeight = maybe_call(player.getVideoHeight, player) || 0,
|
||||
videoWidth = maybe_call(player.getVideoWidth, player) || 0,
|
||||
displayHeight = maybe_call(player.getDisplayHeight, player) || 0,
|
||||
displayWidth = maybe_call(player.getDisplayWidth, player) || 0;
|
||||
|
||||
stats = {
|
||||
backendVersion: maybe_call(player.getVersion, player),
|
||||
bufferSize: maybe_call(player.getBufferDuration, player),
|
||||
displayResolution: `${displayWidth}x${displayHeight}`,
|
||||
videoResolution: `${videoWidth}x${videoHeight}`,
|
||||
videoHeight,
|
||||
videoWidth,
|
||||
displayHeight,
|
||||
displayWidth,
|
||||
rate: maybe_call(player.getPlaybackRate, player),
|
||||
fps: Math.floor(maybe_call(player.getVideoFrameRate, player) || 0),
|
||||
hlsLatencyBroadcaster: maybe_call(player.getLiveLatency, player) || 0,
|
||||
//hlsLatencyBroadcaster: player.stats?.broadcasterLatency || player.core?.stats?.broadcasterLatency,
|
||||
//hlsLatencyEncoder: player.stats?.transcoderLatency || player.core?.stats?.transcoderLatency,
|
||||
playbackRate: Math.floor((maybe_call(player.getVideoBitRate, player) || 0) / 1000),
|
||||
skippedFrames: maybe_call(player.getDroppedFrames, player),
|
||||
}
|
||||
}
|
||||
|
||||
let tampered = false;
|
||||
try {
|
||||
const url = player.core.state.path;
|
||||
if ( url.includes('/api/channel/hls/') ) {
|
||||
const data = JSON.parse(new URL(url).searchParams.get('token'));
|
||||
tampered = data && data.player_type && (data.player_type !== 'site' && data.player_type !== 'popout' && data.player_type !== 'embed') ? data.player_type : false;
|
||||
}
|
||||
} catch(err) { /* no op */ }
|
||||
|
||||
|
||||
if ( ! stats || stats.hlsLatencyBroadcaster < -100 )
|
||||
return {stats};
|
||||
|
||||
return {
|
||||
stats,
|
||||
drift: 0,
|
||||
rate: stats.rate == null ? 1 : stats.rate,
|
||||
delay: stats.hlsLatencyBroadcaster,
|
||||
old: stats.hlsLatencyBroadcaster > 180,
|
||||
tampered
|
||||
}
|
||||
},
|
||||
|
||||
order: 3,
|
||||
|
||||
icon(data) {
|
||||
if ( data.rate > 1 )
|
||||
return 'ffz-i-fast-fw';
|
||||
|
||||
return 'ffz-i-gauge'
|
||||
},
|
||||
|
||||
subtitle: () => this.i18n.t('metadata.player-stats.subtitle', 'Latency'),
|
||||
|
||||
label(data) {
|
||||
if ( ! this.settings.get('metadata.player-stats') || ! data.delay )
|
||||
return null;
|
||||
|
||||
const delayed = data.drift > 5000 ? '(!) ' : '';
|
||||
|
||||
if ( data.old )
|
||||
return `${delayed}${data.delay.toFixed(2)}s old`;
|
||||
else
|
||||
return `${delayed}${data.delay.toFixed(2)}s`;
|
||||
},
|
||||
|
||||
click() {
|
||||
const Player = this.resolve('site.player'),
|
||||
ui = Player.playerUI;
|
||||
|
||||
if ( ! ui )
|
||||
return;
|
||||
|
||||
ui.setStatsOverlay(ui.statsOverlay === 1 ? 0 : 1);
|
||||
},
|
||||
|
||||
color(data) {
|
||||
const setting = this.settings.get('metadata.stream-delay-warning');
|
||||
if ( setting === 0 || ! data.delay || data.old )
|
||||
return;
|
||||
|
||||
if ( data.delay > (setting * 2) )
|
||||
return '#f9b6b6'; //'#ec1313';
|
||||
|
||||
else if ( data.delay > setting )
|
||||
return '#fcb896'; //'#fc7835';
|
||||
},
|
||||
|
||||
tooltip(data) {
|
||||
const tampered = data.tampered ? (<div class="tw-border-t tw-mg-t-05 tw-pd-t-05">
|
||||
{this.i18n.t(
|
||||
'metadata.player-stats.tampered',
|
||||
'Your player has an unexpected player type ({type}), which may affect your viewing experience.',
|
||||
{
|
||||
type: data.tampered
|
||||
}
|
||||
)}
|
||||
</div>) : null;
|
||||
|
||||
const delayed = data.drift > 5000 && (<div class="tw-border-b tw-mg-b-05 tw-pd-b-05">
|
||||
{this.i18n.t(
|
||||
'metadata.player-stats.delay-warning',
|
||||
'Your local clock seems to be off by roughly {count,number} seconds, which could make this inaccurate.',
|
||||
Math.round(data.drift / 10) / 100
|
||||
)}
|
||||
</div>);
|
||||
|
||||
const ff = data.rate > 1 && (<div class="tw-border-b tw-mg-b-05 tw-pd-b-05">
|
||||
{this.i18n.t(
|
||||
'metadata.player-stats.rate-warning',
|
||||
'Playing at {rate,number}x speed to reduce delay.',
|
||||
{rate: data.rate.toFixed(2)}
|
||||
)}
|
||||
</div>);
|
||||
|
||||
if ( ! data.stats || ! data.delay )
|
||||
return [
|
||||
delayed,
|
||||
ff,
|
||||
this.i18n.t('metadata.player-stats.latency-tip', 'Stream Latency'),
|
||||
tampered
|
||||
];
|
||||
|
||||
const stats = data.stats,
|
||||
video_info = this.i18n.t(
|
||||
'metadata.player-stats.video-info',
|
||||
'Video: {videoResolution}p{fps}\nPlayback Rate: {playbackRate,number} Kbps\nDropped Frames:{skippedFrames,number}',
|
||||
stats
|
||||
);
|
||||
|
||||
if ( data.old )
|
||||
return [
|
||||
delayed,
|
||||
this.i18n.t(
|
||||
'metadata.player-stats.video-tip',
|
||||
'Video Information'
|
||||
),
|
||||
<div class="tw-pd-t-05">
|
||||
{this.i18n.t(
|
||||
'metadata.player-stats.broadcast-ago',
|
||||
'Broadcast {count,number}s Ago',
|
||||
data.delay
|
||||
)}
|
||||
</div>,
|
||||
<div class="tw-pd-t-05">
|
||||
{video_info}
|
||||
</div>,
|
||||
tampered
|
||||
];
|
||||
|
||||
return [
|
||||
delayed, ff,
|
||||
this.i18n.t(
|
||||
'metadata.player-stats.latency-tip',
|
||||
'Stream Latency'
|
||||
),
|
||||
<div class="tw-pd-t-05">
|
||||
{video_info}
|
||||
</div>,
|
||||
tampered
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onEnable() {
|
||||
const md = this.tooltips.types.metadata = target => {
|
||||
let el = target;
|
||||
if ( el._ffz_stat )
|
||||
el = el._ffz_stat;
|
||||
else if ( ! el.classList.contains('ffz-stat') ) {
|
||||
el = target.closest('.ffz-stat');
|
||||
target._ffz_stat = el;
|
||||
}
|
||||
|
||||
if ( ! el )
|
||||
return;
|
||||
|
||||
const key = el.dataset.key,
|
||||
def = this.definitions[key];
|
||||
|
||||
return maybe_call(def.tooltip, this, el._ffz_data)
|
||||
};
|
||||
|
||||
md.onShow = (target, tip) => {
|
||||
const el = target._ffz_stat || target;
|
||||
el.tip = tip;
|
||||
};
|
||||
|
||||
md.onHide = target => {
|
||||
const el = target._ffz_stat || target;
|
||||
el.tip = null;
|
||||
el.tip_content = null;
|
||||
}
|
||||
|
||||
md.popperConfig = (target, tip, opts) => {
|
||||
opts.placement = 'bottom';
|
||||
opts.modifiers.flip = {behavior: ['bottom','top']};
|
||||
return opts;
|
||||
}
|
||||
}
|
||||
|
||||
get keys() {
|
||||
return Object.keys(this.definitions);
|
||||
}
|
||||
|
||||
define(key, definition) {
|
||||
this.definitions[key] = definition;
|
||||
this.updateMetadata(key);
|
||||
}
|
||||
|
||||
updateMetadata(keys) {
|
||||
const player = this.resolve('site.player');
|
||||
if ( player )
|
||||
for(const inst of player.Player.instances)
|
||||
player.updateMetadata(inst, keys);
|
||||
}
|
||||
|
||||
async render(key, data, container, timers, refresh_fn) {
|
||||
if ( timers[key] )
|
||||
clearTimeout(timers[key]);
|
||||
|
||||
let el = container.querySelector(`.ffz-stat[data-key="${key}"]`);
|
||||
|
||||
const def = this.definitions[key],
|
||||
destroy = () => {
|
||||
if ( el ) {
|
||||
if ( el.tooltip )
|
||||
el.tooltip.destroy();
|
||||
|
||||
if ( el.popper )
|
||||
el.popper.destroy();
|
||||
|
||||
if ( el._ffz_destroy )
|
||||
el._ffz_destroy();
|
||||
|
||||
el._ffz_destroy = el.tooltip = el.popper = null;
|
||||
el.remove();
|
||||
}
|
||||
};
|
||||
|
||||
if ( ! def || (data._mt || 'channel') !== (def.type || 'channel') )
|
||||
return destroy();
|
||||
|
||||
try {
|
||||
const ref_fn = () => refresh_fn(key);
|
||||
data = {
|
||||
...data,
|
||||
refresh: ref_fn
|
||||
};
|
||||
|
||||
// Process the data if a setup method is defined.
|
||||
if ( def.setup )
|
||||
data = await def.setup.call(this, data);
|
||||
|
||||
// Let's get refresh logic out of the way now.
|
||||
const refresh = maybe_call(def.refresh, this, data);
|
||||
if ( refresh )
|
||||
timers[key] = setTimeout(
|
||||
ref_fn,
|
||||
typeof refresh === 'number' ? refresh : 1000
|
||||
);
|
||||
|
||||
|
||||
// Grab the element again in case it changed, somehow.
|
||||
el = container.querySelector(`.ffz-stat[data-key="${key}"]`);
|
||||
|
||||
let stat, old_color, old_icon;
|
||||
|
||||
const label = maybe_call(def.label, this, data);
|
||||
|
||||
if ( ! label )
|
||||
return destroy();
|
||||
|
||||
const order = maybe_call(def.order, this, data),
|
||||
color = maybe_call(def.color, this, data) || '';
|
||||
|
||||
if ( ! el ) {
|
||||
let icon = old_icon = maybe_call(def.icon, this, data);
|
||||
let button = false;
|
||||
|
||||
if ( def.button !== false && (def.popup || def.click) ) {
|
||||
button = true;
|
||||
|
||||
let btn, popup;
|
||||
const border = maybe_call(def.border, this, data),
|
||||
inherit = maybe_call(def.inherit, this, data);
|
||||
|
||||
if ( typeof icon === 'string' )
|
||||
icon = (<span class="tw-mg-r-05">
|
||||
<figure class={icon} />
|
||||
</span>);
|
||||
|
||||
if ( def.popup && def.click ) {
|
||||
el = (<div
|
||||
class={`tw-align-items-center tw-inline-flex tw-relative tw-tooltip__container ffz-stat tw-stat ffz-stat--fix-padding ${border ? 'tw-mg-r-1' : 'tw-mg-r-05 ffz-mg-l--05'}`}
|
||||
data-key={key}
|
||||
tip_content={null}
|
||||
>
|
||||
{btn = (<button
|
||||
class={`tw-align-items-center tw-align-middle tw-border-bottom-left-radius-medium tw-border-top-left-radius-medium tw-core-button tw-core-button--padded tw-core-button--text ${inherit ? 'ffz-c-text-inherit' : 'tw-c-text-base'} tw-inline-flex tw-interactive tw-justify-content-center tw-overflow-hidden tw-relative ${border ? 'tw-border-l tw-border-t tw-border-b' : 'tw-regular'}${def.tooltip ? ' ffz-tooltip ffz-tooltip--no-mouse' : ''}`}
|
||||
data-tooltip-type="metadata"
|
||||
>
|
||||
<div class="tw-align-items-center tw-flex tw-flex-grow-0 tw-justify-center tw-pd-x-1">
|
||||
{icon}
|
||||
{stat = (<span class="ffz-stat-text" />)}
|
||||
</div>
|
||||
</button>)}
|
||||
{popup = (<button
|
||||
class={`tw-align-items-center tw-align-middle tw-border-bottom-right-radius-medium tw-border-top-right-radius-medium tw-core-button tw-core-button--text ${inherit ? 'ffz-c-text-inherit' : 'tw-c-text-base'} tw-inline-flex tw-interactive tw-justify-content-center tw-overflow-hidden tw-relative ${border ? 'tw-border' : 'tw-regular'}${def.tooltip ? ' ffz-tooltip ffz-tooltip--no-mouse' : ''}`}
|
||||
data-tooltip-type="metadata"
|
||||
>
|
||||
<div class="tw-align-items-center tw-flex tw-flex-grow-0 tw-justify-center">
|
||||
<span>
|
||||
<figure class="ffz-i-down-dir" />
|
||||
</span>
|
||||
</div>
|
||||
</button>)}
|
||||
</div>);
|
||||
|
||||
} else
|
||||
btn = popup = el = (<button
|
||||
class={`ffz-stat tw-align-items-center tw-align-middle ${inherit ? 'ffz-c-text-inherit' : ''} tw-inline-flex tw-interactive tw-justify-content-center tw-overflow-hidden tw-relative ffz-stat--fix-padding ${border ? 'tw-border tw-mg-r-1' : 'tw-regular tw-mg-r-05 ffz-mg-l--05'}${def.tooltip ? ' ffz-tooltip ffz-tooltip--no-mouse' : ''}`}
|
||||
data-tooltip-type="metadata"
|
||||
data-key={key}
|
||||
tip_content={null}
|
||||
>
|
||||
<div class="tw-align-items-center tw-flex tw-flex-grow-0 tw-justify-center">
|
||||
{icon}
|
||||
{stat = (<span class="ffz-stat-text" />)}
|
||||
{def.popup && ! def.no_arrow && <span class="tw-mg-l-05">
|
||||
<figure class="ffz-i-down-dir" />
|
||||
</span>}
|
||||
</div>
|
||||
</button>);
|
||||
|
||||
if ( def.click )
|
||||
btn.addEventListener('click', e => {
|
||||
if ( el._ffz_fading || btn.disabled || btn.classList.contains('disabled') || el.disabled || el.classList.contains('disabled') )
|
||||
return false;
|
||||
|
||||
def.click.call(this, el._ffz_data, e, () => refresh_fn(key));
|
||||
});
|
||||
|
||||
if ( def.popup )
|
||||
popup.addEventListener('click', () => {
|
||||
if ( popup.disabled || popup.classList.contains('disabled') || el.disabled || el.classList.contains('disabled') )
|
||||
return false;
|
||||
|
||||
if ( el._ffz_popup )
|
||||
return el._ffz_destroy();
|
||||
|
||||
const listeners = [],
|
||||
add_close_listener = cb => listeners.push(cb);
|
||||
|
||||
const destroy = el._ffz_destroy = () => {
|
||||
for(const cb of listeners) {
|
||||
try {
|
||||
cb();
|
||||
} catch(err) {
|
||||
this.log.capture(err, {
|
||||
tags: {
|
||||
metadata: key
|
||||
}
|
||||
});
|
||||
this.log.error('Error when running a callback for pop-up destruction for metadata:', key, err);
|
||||
}
|
||||
}
|
||||
|
||||
if ( el._ffz_outside )
|
||||
el._ffz_outside.destroy();
|
||||
|
||||
if ( el._ffz_popup ) {
|
||||
const fp = el._ffz_popup;
|
||||
el._ffz_popup = null;
|
||||
fp.destroy();
|
||||
}
|
||||
|
||||
el._ffz_destroy = el._ffz_outside = null;
|
||||
};
|
||||
|
||||
const parent = document.fullscreenElement || document.body.querySelector('#root>div') || document.body,
|
||||
tt = el._ffz_popup = new Tooltip(parent, el, {
|
||||
logger: this.log,
|
||||
i18n: this.i18n,
|
||||
manual: true,
|
||||
live: false,
|
||||
html: true,
|
||||
|
||||
tooltipClass: 'ffz-metadata-balloon ffz-balloon tw-block tw-border tw-elevation-1 tw-border-radius-small tw-c-background-base',
|
||||
// Hide the arrow for now, until we re-do our CSS to make it render correctly.
|
||||
arrowClass: 'ffz-balloon__tail tw-overflow-hidden tw-absolute',
|
||||
arrowInner: 'ffz-balloon__tail-symbol tw-border-t tw-border-r tw-border-b tw-border-l tw-border-radius-small tw-c-background-base tw-absolute',
|
||||
innerClass: 'tw-pd-1',
|
||||
|
||||
popper: {
|
||||
placement: 'top-end',
|
||||
modifiers: {
|
||||
preventOverflow: {
|
||||
boundariesElement: parent
|
||||
},
|
||||
flip: {
|
||||
behavior: ['top', 'bottom', 'left', 'right']
|
||||
}
|
||||
}
|
||||
},
|
||||
content: (t, tip) => def.popup.call(this, el._ffz_data, tip, () => refresh_fn(key), add_close_listener),
|
||||
onShow: (t, tip) =>
|
||||
setTimeout(() => {
|
||||
el._ffz_outside = new ClickOutside(tip.outer, destroy);
|
||||
}),
|
||||
onHide: destroy
|
||||
});
|
||||
|
||||
tt._enter(el);
|
||||
});
|
||||
|
||||
} else {
|
||||
if ( typeof icon === 'string' )
|
||||
icon = (<span class="tw-stat__icon"><figure class={icon} /></span>);
|
||||
|
||||
el = (<div
|
||||
class={`tw-align-items-center tw-inline-flex tw-relative tw-tooltip__container ffz-stat tw-stat tw-mg-r-1${def.tooltip ? ' ffz-tooltip ffz-tooltip--no-mouse' : ''}`}
|
||||
data-tooltip-type="metadata"
|
||||
data-key={key}
|
||||
tip_content={null}
|
||||
>
|
||||
{icon}
|
||||
{stat = <span class={`${icon ? 'tw-mg-l-05 ' : ''}ffz-stat-text tw-stat__value`} />}
|
||||
</div>);
|
||||
|
||||
if ( def.click )
|
||||
el.addEventListener('click', e => {
|
||||
if ( el._ffz_fading || el.disabled || el.classList.contains('disabled') )
|
||||
return false;
|
||||
|
||||
def.click.call(this, el._ffz_data, e, () => refresh_fn(key));
|
||||
});
|
||||
}
|
||||
|
||||
el._ffz_order = order;
|
||||
|
||||
if ( order != null )
|
||||
el.style.order = order;
|
||||
|
||||
let subcontainer = container;
|
||||
|
||||
/*if ( button )
|
||||
subcontainer = container.querySelector('.tw-flex:last-child') || container;
|
||||
else
|
||||
subcontainer = container.querySelector('.tw-flex:first-child') || container;*/
|
||||
|
||||
subcontainer.appendChild(el);
|
||||
|
||||
/*if ( def.tooltip ) {
|
||||
const parent = document.body.querySelector('#root>div') || document.body;
|
||||
el.tooltip = new Tooltip(parent, el, {
|
||||
logger: this.log,
|
||||
live: false,
|
||||
html: true,
|
||||
content: () => maybe_call(def.tooltip, this, el._ffz_data),
|
||||
onShow: (t, tip) => el.tip = tip,
|
||||
onHide: () => {
|
||||
el.tip = null;
|
||||
el.tip_content = null;
|
||||
},
|
||||
popper: {
|
||||
placement: 'bottom',
|
||||
modifiers: {
|
||||
flip: {
|
||||
behavior: ['bottom', 'top']
|
||||
},
|
||||
preventOverflow: {
|
||||
boundariesElement: parent
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}*/
|
||||
|
||||
} else {
|
||||
stat = el.querySelector('.ffz-stat-text');
|
||||
if ( ! stat )
|
||||
return destroy();
|
||||
|
||||
old_icon = el.dataset.icon || '';
|
||||
old_color = el.dataset.color || '';
|
||||
|
||||
if ( el._ffz_order !== order )
|
||||
el.style.order = el._ffz_order = order;
|
||||
|
||||
if ( el.tip ) {
|
||||
const tooltip = maybe_call(def.tooltip, this, data);
|
||||
if ( el.tip_content !== tooltip ) {
|
||||
el.tip_content = tooltip;
|
||||
setChildren(el.tip.element, tooltip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( typeof def.icon === 'function' ) {
|
||||
const icon = maybe_call(def.icon, this, data);
|
||||
if ( typeof icon === 'string' && icon !== old_icon ) {
|
||||
el.dataset.icon = icon;
|
||||
const figure = el.querySelector('figure');
|
||||
if ( figure )
|
||||
figure.className = icon;
|
||||
}
|
||||
}
|
||||
|
||||
if ( old_color !== color ) {
|
||||
el.dataset.color = color;
|
||||
el.style.setProperty('color', color, 'important');
|
||||
}
|
||||
|
||||
el._ffz_data = data;
|
||||
stat.innerHTML = label;
|
||||
|
||||
if ( def.disabled !== undefined )
|
||||
el.disabled = maybe_call(def.disabled, this, data);
|
||||
|
||||
} catch(err) {
|
||||
this.log.capture(err, {
|
||||
tags: {
|
||||
metadata: key
|
||||
}
|
||||
});
|
||||
this.log.error(`Error rendering metadata for ${key}`, err);
|
||||
return destroy();
|
||||
}
|
||||
}
|
||||
}
|
1302
src/sites/player/player.jsx
Normal file
1302
src/sites/player/player.jsx
Normal file
File diff suppressed because it is too large
Load diff
50
src/sites/player/styles/player-main.scss
Normal file
50
src/sites/player/styles/player-main.scss
Normal file
|
@ -0,0 +1,50 @@
|
|||
@import 'styles/main.scss';
|
||||
|
||||
.ffz--player-pip,
|
||||
.ffz--player-reset {
|
||||
&:before {
|
||||
font-size: 2rem;
|
||||
margin-top: .7rem;
|
||||
}
|
||||
|
||||
&:hover:before {
|
||||
color: #a991d4;
|
||||
}
|
||||
}
|
||||
|
||||
.player-controls-bottom .player-tip {
|
||||
top: auto;
|
||||
bottom: 4rem;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
div[data-a-target="player-settings-menu"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ffz--cc-button {
|
||||
order: 999;
|
||||
}
|
||||
|
||||
.ffz--player-meta-tray {
|
||||
display: inline-flex;
|
||||
color: var(--color-text-overlay);
|
||||
}
|
||||
|
||||
.ffz-stat {
|
||||
background-color: var(--color-background-pill);
|
||||
color: var(--color-text-overlay);
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
border-radius: 1000px;
|
||||
padding: 0.3rem 0.5rem;
|
||||
font-size: 75%;
|
||||
}
|
||||
|
||||
.ffz-stat-text {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
|
@ -32,8 +32,14 @@ export default class Clippy extends BaseSite {
|
|||
//this.inject(Switchboard);
|
||||
}
|
||||
|
||||
onLoad() {
|
||||
this.populateModules();
|
||||
async populateModules() {
|
||||
const ctx = await require.context('site/modules', true, /(?:^(?:\.\/)?[^/]+|index)\.jsx?$/);
|
||||
const modules = await this.populate(ctx, this.log);
|
||||
this.log.info(`Loaded descriptions of ${Object.keys(modules).length} modules.`);
|
||||
}
|
||||
|
||||
async onLoad() {
|
||||
await this.populateModules();
|
||||
}
|
||||
|
||||
onEnable() {
|
||||
|
|
|
@ -42,6 +42,12 @@ export default class Twilight extends BaseSite {
|
|||
this._dom_updates = [];
|
||||
}
|
||||
|
||||
async populateModules() {
|
||||
const ctx = await require.context('site/modules', true, /(?:^(?:\.\/)?[^/]+|index)\.jsx?$/);
|
||||
const modules = await this.populate(ctx, this.log);
|
||||
this.log.info(`Loaded descriptions of ${Object.keys(modules).length} modules.`);
|
||||
}
|
||||
|
||||
async onLoad() {
|
||||
await this.populateModules();
|
||||
|
||||
|
@ -54,6 +60,7 @@ export default class Twilight extends BaseSite {
|
|||
this.router.route('user-home', '/:userName', null, state => state?.channelView === 'Home');
|
||||
|
||||
this.router.route(Twilight.DASH_ROUTES, 'dashboard.twitch.tv');
|
||||
this.router.route(Twilight.PLAYER_ROUTES, 'player.twitch.tv');
|
||||
}
|
||||
|
||||
onEnable() {
|
||||
|
@ -237,7 +244,8 @@ Twilight.ROUTE_NAMES = {
|
|||
'dash': 'Dashboard',
|
||||
'popout': 'Popout Chat',
|
||||
'dash-chat': 'Dashboard Popout Chat',
|
||||
'user-video': 'Channel Video'
|
||||
'user-video': 'Channel Video',
|
||||
'popout-player': 'Popout/Embed Player'
|
||||
};
|
||||
|
||||
|
||||
|
@ -262,6 +270,11 @@ Twilight.SUNLIGHT_ROUTES = [
|
|||
];
|
||||
|
||||
|
||||
Twilight.PLAYER_ROUTES = {
|
||||
'popout-player': '/'
|
||||
};
|
||||
|
||||
|
||||
Twilight.DASH_ROUTES = {
|
||||
'dash-stream-manager': '/u/:userName/stream-manager',
|
||||
'dash-channel-analytics': '/u/:userName/channel-analytics',
|
||||
|
|
|
@ -53,7 +53,6 @@ export default class Player extends Module {
|
|||
this.inject('settings');
|
||||
|
||||
this.inject('site.fine');
|
||||
this.inject('site.web_munch');
|
||||
this.inject('site.css_tweaks');
|
||||
this.inject('site.router');
|
||||
|
||||
|
@ -92,6 +91,15 @@ export default class Player extends Module {
|
|||
|
||||
// Settings
|
||||
|
||||
this.settings.add('player.embed-metadata', {
|
||||
default: true,
|
||||
ui: {
|
||||
path: 'Player > General >> Embed and Popout',
|
||||
title: 'Show metadata when mousing over the player.',
|
||||
component: 'setting-check-box'
|
||||
}
|
||||
});
|
||||
|
||||
if ( HAS_COMPRESSOR ) {
|
||||
this.settings.add('player.compressor.enable', {
|
||||
default: true,
|
||||
|
@ -1207,8 +1215,14 @@ export default class Player extends Module {
|
|||
|
||||
let comp = video._ffz_compressor;
|
||||
if ( ! comp ) {
|
||||
const ctx = video._ffz_context = new AudioContext(),
|
||||
src = video._ffz_source = ctx.createMediaElementSource(video);
|
||||
const ctx = new AudioContext();
|
||||
if ( ctx.state === 'suspended' ) {
|
||||
this.log.info('Aborting due to browser auto-play policy.');
|
||||
return;
|
||||
}
|
||||
|
||||
video._ffz_context = ctx;
|
||||
const src = video._ffz_source = ctx.createMediaElementSource(video);
|
||||
|
||||
src.connect(ctx.destination);
|
||||
|
||||
|
|
|
@ -17,8 +17,14 @@ export function createTester(rules, filter_types, inverted = false, or = false,
|
|||
continue;
|
||||
|
||||
const type = filter_types[rule.type];
|
||||
if ( ! type )
|
||||
if ( ! type ) {
|
||||
// Default to false if we cannot find a rule type.
|
||||
// Just to be safe.
|
||||
i++;
|
||||
tests.push(() => false);
|
||||
names.push(`f${i}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
i++;
|
||||
tests.push(type.createTest(rule.data, filter_types, rebuild));
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue