1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-08-02 16:08:31 +00:00
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:
SirStendec 2021-02-12 15:27:12 -05:00
parent 264c375f13
commit a473c6eb93
23 changed files with 2362 additions and 49 deletions

View file

@ -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": {

View file

@ -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/' : '',

View file

@ -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) ?

View file

@ -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',

View file

@ -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;
}
}
}
}

View file

@ -1,4 +1,4 @@
'use strict';
'use strict';
// ============================================================================
// Channel Metadata

View file

@ -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();

View file

@ -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;

View file

@ -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)';
}

View file

@ -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

View 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();
})
}
}

View file

@ -0,0 +1,3 @@
'use strict';
export default require.context('!raw-loader!sass-loader!./styles', false, /\.s?css$/);

View file

@ -0,0 +1,5 @@
.video-player .extension-overlay__iframe,
.video-player .extension-overlay,
.video-player .extension-view__iframe {
pointer-events: none !important;
}

View file

@ -0,0 +1,3 @@
.video-player__overlay[data-controls="false"][data-paused="false"][data-ended="false"] {
cursor: none;
}

View file

@ -0,0 +1,3 @@
.video-player .volume-slider__slider-container {
opacity: 1 !important;
}

157
src/sites/player/index.jsx Normal file
View 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');
}
}

View 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

File diff suppressed because it is too large Load diff

View 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;
}

View file

@ -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() {

View file

@ -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',

View file

@ -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);

View file

@ -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));