From 6547497d02b2c149f32bbedf2b399510caa17405 Mon Sep 17 00:00:00 2001 From: SirStendec Date: Thu, 17 Oct 2019 17:07:58 -0400 Subject: [PATCH] 4.14.6 * Fixed: Rewrite our player code to support the Highwind player. This removes support for the old player, but to our knowledge everyone should be using Highwind now. * Removed: The Reset Player button. At this time we just don't know how to properly implement it for the new player. The new player is more tightly integrated into the Twitch web application. * Removed: Closed Captioning position settings. Twitch appears to have native position controls for closed captions now. --- package.json | 2 +- src/modules/metadata.jsx | 34 +- .../modules/css_tweaks/index.js | 4 +- .../css_tweaks/styles/global-font.scss | 3 + .../css_tweaks/styles/player-ext-mouse.scss | 5 +- .../css_tweaks/styles/player-hide-mouse.scss | 1 + .../css_tweaks/styles/player-volume.scss | 1 + .../styles/theatre-no-whispers.scss | 3 +- src/sites/twitch-twilight/modules/player.jsx | 820 ++++++++---------- src/utilities/compat/fine.js | 35 +- 10 files changed, 433 insertions(+), 475 deletions(-) create mode 100644 src/sites/twitch-twilight/modules/css_tweaks/styles/global-font.scss diff --git a/package.json b/package.json index 1442de59..4c66a905 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frankerfacez", "author": "Dan Salvato LLC", - "version": "4.14.5", + "version": "4.14.6", "description": "FrankerFaceZ is a Twitch enhancement suite.", "license": "Apache-2.0", "scripts": { diff --git a/src/modules/metadata.jsx b/src/modules/metadata.jsx index 8a453f5e..d3840169 100644 --- a/src/modules/metadata.jsx +++ b/src/modules/metadata.jsx @@ -155,6 +155,27 @@ export default class Metadata extends Module { skippedFrames: temp.dropped_frames, videoResolution: `${temp.vid_width}x${temp.vid_height}` } + } else if ( player.stats ) { + 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, + fps: (maybe_call(player.getVideoFrameRate, player) || 0).toFixed(2), + hlsLatencyBroadcaster: player.stats?.broadcasterLatency, + hlsLatencyEncoder: player.stats?.transcoderLatency, + playbackRate: (maybe_call(player.getVideoBitRate, player) || 0) / 1000, + skippedFrames: maybe_call(player.getDroppedFrames, player), + } } if ( ! stats || stats.hlsLatencyBroadcaster < -100 ) @@ -192,19 +213,12 @@ export default class Metadata extends Module { click() { const Player = this.resolve('site.player'), - internal = Player.getInternalPlayer(); + ui = Player.playerUI; - if ( ! internal ) + if ( ! ui ) return; - const store = internal.context.store, - state = store.getState(), - displayed = state && state.stats && state.stats.displayState === 'DISPLAY_VIDEO_STATS'; - - store.dispatch({ - type: 'display stats', - displayState: displayed ? 'DISPLAY_NONE' : 'DISPLAY_VIDEO_STATS' - }); + ui.setStatsOverlay(ui.statsOverlay === 1 ? 0 : 1); }, color(data) { diff --git a/src/sites/twitch-twilight/modules/css_tweaks/index.js b/src/sites/twitch-twilight/modules/css_tweaks/index.js index 02839276..17d6b50a 100644 --- a/src/sites/twitch-twilight/modules/css_tweaks/index.js +++ b/src/sites/twitch-twilight/modules/css_tweaks/index.js @@ -22,8 +22,8 @@ const CLASSES = { 'prime-offers': '.top-nav__prime', - 'player-ext': '.player .extension-taskbar,.player .extension-container,.player .extensions-dock__layout,.player .extensions-notifications,.player .extensions-video-overlay-size-container', - 'player-ext-hover': '.player:not([data-controls="true"]) .extension-container,.player:not([data-controls="true"]) .extensions-dock__layout,.player:not([data-controls="true"]) .extensions-notifications,.player:not([data-controls="true"]) .extensions-video-overlay-size-container', + 'player-ext': '.highwind-video-player .extension-taskbar,.highwind-video-player .extension-container,.highwind-video-player .extensions-dock__layout,.highwind-video-player .extensions-notifications,.highwind-video-player .extensions-video-overlay-size-container', + 'player-ext-hover': '.highwind-video-player__overlay[data-controls="false"] .extension-taskbar,.highwind-video-player__overlay[data-controls="false"] .extension-container,.highwind-video-player__overlay[data-controls="false"] .extensions-dock__layout,.highwind-video-player__overlay[data-controls="false"] .extensions-notifications,.highwind-video-player__overlay[data-controls="false"] .extensions-video-overlay-size-container', 'player-event-bar': '.channel-root .live-event-banner-ui__header', 'player-rerun-bar': '.channel-root__player-container div.tw-c-text-overlay:not([data-a-target="hosting-ui-header"])', diff --git a/src/sites/twitch-twilight/modules/css_tweaks/styles/global-font.scss b/src/sites/twitch-twilight/modules/css_tweaks/styles/global-font.scss new file mode 100644 index 00000000..70621d13 --- /dev/null +++ b/src/sites/twitch-twilight/modules/css_tweaks/styles/global-font.scss @@ -0,0 +1,3 @@ +html body { + font-family: var(--ffz-global-font) !important; +} \ No newline at end of file diff --git a/src/sites/twitch-twilight/modules/css_tweaks/styles/player-ext-mouse.scss b/src/sites/twitch-twilight/modules/css_tweaks/styles/player-ext-mouse.scss index 88944864..e3f413a0 100644 --- a/src/sites/twitch-twilight/modules/css_tweaks/styles/player-ext-mouse.scss +++ b/src/sites/twitch-twilight/modules/css_tweaks/styles/player-ext-mouse.scss @@ -1,4 +1,7 @@ .player .extension-overlay__iframe, -.player .extension-overlay { +.player .extension-overlay, +.highwind-video-player .extension-overlay__iframe, +.highwind-video-player .extension-overlay, +.highwind-video-player .extension-view__iframe { pointer-events: none !important; } \ No newline at end of file diff --git a/src/sites/twitch-twilight/modules/css_tweaks/styles/player-hide-mouse.scss b/src/sites/twitch-twilight/modules/css_tweaks/styles/player-hide-mouse.scss index 37389c7f..d6bc3470 100644 --- a/src/sites/twitch-twilight/modules/css_tweaks/styles/player-hide-mouse.scss +++ b/src/sites/twitch-twilight/modules/css_tweaks/styles/player-hide-mouse.scss @@ -1,3 +1,4 @@ +.highwind-video-player__overlay[data-controls="false"][data-paused="false"][data-ended="false"], .player:hover:not([data-controls="true"]):not([data-paused="true"]):not([data-ended="true"]) { cursor: none; } \ No newline at end of file diff --git a/src/sites/twitch-twilight/modules/css_tweaks/styles/player-volume.scss b/src/sites/twitch-twilight/modules/css_tweaks/styles/player-volume.scss index 9c671e42..d714c63f 100644 --- a/src/sites/twitch-twilight/modules/css_tweaks/styles/player-volume.scss +++ b/src/sites/twitch-twilight/modules/css_tweaks/styles/player-volume.scss @@ -1,3 +1,4 @@ +.highwind-video-player .volume-slider__slider-container, .player .player-volume__slider-container { opacity: 1 !important; } \ No newline at end of file diff --git a/src/sites/twitch-twilight/modules/css_tweaks/styles/theatre-no-whispers.scss b/src/sites/twitch-twilight/modules/css_tweaks/styles/theatre-no-whispers.scss index 2e756969..33fde287 100644 --- a/src/sites/twitch-twilight/modules/css_tweaks/styles/theatre-no-whispers.scss +++ b/src/sites/twitch-twilight/modules/css_tweaks/styles/theatre-no-whispers.scss @@ -1,5 +1,6 @@ +.highwind-video-player__container--theatre-whispers, .video-player--theatre.video-player--logged-in .player.video-player__container { - bottom: 0; + bottom: 0 !important; } .whispers.whispers--theatre-mode { display: none !important } \ No newline at end of file diff --git a/src/sites/twitch-twilight/modules/player.jsx b/src/sites/twitch-twilight/modules/player.jsx index ba39da04..f60db935 100644 --- a/src/sites/twitch-twilight/modules/player.jsx +++ b/src/sites/twitch-twilight/modules/player.jsx @@ -17,26 +17,19 @@ export default class Player extends Module { this.should_enable = true; + + // Dependency Injection + + this.inject('i18n'); this.inject('settings'); + this.inject('site.fine'); this.inject('site.web_munch'); this.inject('site.css_tweaks'); this.inject('site.router'); - this.inject('i18n'); - - this.Player = this.fine.define( - 'twitch-player', - n => n.player && n.onPlayerReady, - PLAYER_ROUTES - ); - // TODO: Better way to reposition player on demand. - this.PersistentPlayer = this.fine.define( - 'twitch-player-persistent', - n => n.renderMiniHoverControls && n.togglePause, - PLAYER_ROUTES - ); + // React Components this.SquadStreamBar = this.fine.define( 'squad-stream-bar', @@ -44,6 +37,21 @@ export default class Player extends Module { PLAYER_ROUTES ); + this.Player = this.fine.define( + 'highwind-player', + n => n.setPlayerActive && n.props?.playerEvents && n.props?.mediaPlayerInstance, + PLAYER_ROUTES + ); + + this.TheatreHost = this.fine.define( + 'theatre-host', + n => n.toggleTheatreMode && n.props && n.props.onTheatreModeEnabled, + ['user', 'video', 'user-video', 'user-clip'] + ); + + + // Settings + this.settings.add('player.volume-scroll', { default: false, ui: { @@ -59,7 +67,7 @@ export default class Player extends Module { } }); - this.settings.add('player.button.reset', { + /*this.settings.add('player.button.reset', { default: true, ui: { path: 'Player > General >> General', @@ -71,7 +79,7 @@ export default class Player extends Module { for(const inst of this.Player.instances) this.addResetButton(inst); } - }); + });*/ if ( document.pictureInPictureEnabled ) this.settings.add('player.button.pip', { @@ -126,7 +134,7 @@ export default class Player extends Module { changed: () => this.updateCaptionsCSS() }); - this.settings.add('player.captions.custom-position', { + /*this.settings.add('player.captions.custom-position', { default: false, ui: { path: 'Player > Closed Captioning >> Position', @@ -178,7 +186,7 @@ export default class Player extends Module { ] }, changed: () => this.updateCaptionsCSS() - }); + });*/ this.settings.add('player.theatre.no-whispers', { default: false, @@ -292,7 +300,7 @@ export default class Player extends Module { }, changed: val => { this.css_tweaks.toggleHide('player-event-bar', val); - this.PersistentPlayer.forceUpdate(); + this.repositionPlayer(); } }); @@ -306,7 +314,7 @@ export default class Player extends Module { }, changed: val => { this.css_tweaks.toggleHide('player-rerun-bar', val); - this.PersistentPlayer.forceUpdate(); + this.repositionPlayer(); } }); @@ -331,6 +339,264 @@ export default class Player extends Module { }); } + onEnable() { + 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('theatre-no-whispers', this.settings.get('player.theatre.no-whispers')); + this.css_tweaks.toggle('theatre-metadata', this.settings.get('player.theatre.metadata')); + this.css_tweaks.toggle('player-hide-mouse', this.settings.get('player.hide-mouse')); + this.css_tweaks.toggleHide('player-event-bar', this.settings.get('player.hide-event-bar')); + this.css_tweaks.toggleHide('player-rerun-bar', this.settings.get('player.hide-rerun-bar')); + + this.updateCaptionsCSS(); + this.updateHideExtensions(); + this.installVisibilityHook(); + + const t = this; + + + this.SquadStreamBar.ready(cls => { + const old_should_render = cls.prototype.shouldRenderSquadBanner; + + cls.prototype.shouldRenderSquadBanner = function(...args) { + if ( t.settings.get('player.hide-squad-banner') ) + return false; + + return old_should_render.call(this, ...args); + } + + this.SquadStreamBar.forceUpdate(); + this.updateSquadContext(); + }); + + this.SquadStreamBar.on('mount', this.updateSquadContext, this); + this.SquadStreamBar.on('update', this.updateSquadContext, this); + this.SquadStreamBar.on('unmount', this.updateSquadContext, this); + + + this.Player.ready((cls, instances) => { + const old_attach = cls.prototype.maybeAttachDomEventListeners; + + cls.prototype.ffzInstall = function() { + if ( this._ffz_installed ) + return; + + this._ffz_installed = true; + + if ( ! this._ffzUpdateState ) + this._ffzUpdateState = this.ffzUpdateState.bind(this); + + const inst = this, + old_active = this.setPlayerActive, + old_inactive = this.setPlayerInactive; + + this.setPlayerActive = function() { + inst.ffzScheduleState(); + return old_active.call(inst); + } + + this.setPlayerInactive = function() { + inst.ffzScheduleState(); + return old_inactive.call(inst); + } + + this.ffzOnEnded = () => { + if ( t.settings.get('player.vod.autoplay') ) + return; + + t.parent.awaitElement( + '.autoplay-vod__content-container button', + this.props.containerRef || t.fine.getChildNode(this), + 1000 + ).then(el => el.click()); + } + + const events = this.props.playerEvents; + if ( events ) { + on(events, 'Playing', this._ffzUpdateState); + on(events, 'PlayerError', this._ffzUpdateState); + on(events, 'Ended', this._ffzUpdateState); + on(events, 'Ended', this.ffzOnEnded); + on(events, 'Idle', this._ffzUpdateState); + } + + this.ffzStopAutoplay(); + } + + cls.prototype.ffzUninstall = function() { + if ( this._ffz_state_raf ) + cancelAnimationFrame(this._ffz_state_raf); + + const events = this.props.playerEvents; + if ( events && this._ffzUpdateState ) { + off(events, 'Playing', this._ffzUpdateState); + off(events, 'PlayerError', this._ffzUpdateState); + off(events, 'Ended', this._ffzUpdateState); + off(events, 'Ended', this.ffzOnEnded); + off(events, 'Idle', this._ffzUpdateState); + } + + this.ffzRemoveListeners(); + + this._ffz_state_raf = null; + this._ffzUpdateState = null; + this.ffzOnEnded = null; + } + + cls.prototype.ffzStopAutoplay = function() { + if ( t.settings.get('player.no-autoplay') || (! t.settings.get('player.home.autoplay') && t.router.current.name === 'front-page') ) { + const player = this.props.mediaPlayerInstance, + events = this.props.playerEvents; + + if ( player && player.pause ) + player.pause(); + else if ( events ) { + const immediatePause = () => { + if ( this.props.mediaPlayerInstance?.pause ) { + this.props.mediaPlayerInstance.pause(); + off(events, 'Playing', immediatePause); + } + } + + t.log.info('Unable to immediately pause. Listening for playing event.'); + on(events, 'Playing', immediatePause); + } + } + } + + cls.prototype.ffzScheduleState = function() { + if ( ! this._ffzUpdateState ) + this._ffzUpdateState = this.ffzUpdateState.bind(this); + + if ( ! this._ffz_state_raf ) + this._ffz_state_raf = requestAnimationFrame(this._ffzUpdateState); + } + + cls.prototype.ffzUpdateState = function() { + this._ffz_state_raf = null; + const cont = this.props.containerRef, + player = this.props.mediaPlayerInstance; + if ( ! cont ) + return; + + const ds = cont.dataset; + ds.controls = this.state?.active || false; + + ds.ended = player?.state?.playerState === 'Ended'; + ds.paused = player?.state?.playerState === 'Idle'; + } + + cls.prototype.ffzAttachListeners = function() { + const cont = this.props.containerRef; + if ( ! cont || this._ffz_listeners ) + return; + + this._ffz_listeners = true; + if ( ! this._ffz_scroll_handler ) + this._ffz_scroll_handler = this.ffzScrollHandler.bind(this); + on(cont, 'wheel', this._ffz_scroll_handler); + } + + cls.prototype.ffzRemoveListeners = function() { + const cont = this.props.containerRef; + if ( ! cont || ! this._ffz_listeners ) + return; + + if ( this._ffz_scroll_handler ) { + off(cont, 'wheel', this._ffz_scroll_handler); + this._ffz_scroll_handler = null; + } + + this._ffz_listeners = false; + } + + cls.prototype.ffzScrollHandler = function(event) { + if ( ! t.settings.get('player.volume-scroll') ) + return; + + const delta = event.wheelDelta || -(event.deltaY || event.detail || 0), + player = this.props?.mediaPlayerInstance; + + if ( ! player?.getVolume ) + return; + + const amount = t.settings.get('player.volume-scroll-steps'), + volume = Math.max(0, Math.min(1, player.getVolume() + (delta > 0 ? amount : -amount))); + + player.setVolume(volume); + if ( volume !== 0 ) + player.setMuted(false); + + event.preventDefault(); + return false; + } + + cls.prototype.ffzMaybeRemoveNativeListeners = function() { + const cont = this.props.containerRef; + if ( cont && this.listenersAttached ) { + off(cont, 'mouseleave', this.setPlayerInactive); + off(cont, 'mouseenter', this.setPlayerActive); + off(cont, 'mousemove', this.onMouseMove); + this.listenersAttached = false; + } + } + + cls.prototype.maybeAttachDomEventListeners = function() { + try { + this.ffzInstall(); + this.ffzAttachListeners(); + } catch(err) { + t.log.error('Error attaching event listener.', err); + } + + return old_attach.call(this); + } + + + for(const inst of instances) { + const events = inst.props?.playerEvents; + if ( events ) { + off(events, 'Playing', inst.setPlayerActive); + off(events, 'PlayerSeekCompleted', inst.setPlayerActive); + } + + inst.ffzMaybeRemoveNativeListeners(); + inst.maybeAttachDomEventListeners(); + inst.ffzScheduleState(); + + if ( events ) { + on(events, 'Playing', inst.setPlayerActive); + on(events, 'PlayerSeekCompleted', inst.setPlayerActive); + } + + this.updateGUI(inst); + } + }); + + this.Player.on('mount', inst => { + this.updateGUI(inst); + }); + + this.Player.on('unmount', inst => { + inst.ffzUninstall(); + }); + + + this.TheatreHost.on('mount', this.tryTheatreMode, this); + this.TheatreHost.ready((cls, instances) => { + for(const inst of instances) + this.tryTheatreMode(inst); + }); + + + this.on('i18n:update', () => { + for(const inst of this.Player.instances) { + this.updateGUI(inst); + } + }) + } + + updateHideExtensions(val) { if ( val === undefined ) val = this.settings.get('player.ext-hide'); @@ -339,6 +605,7 @@ export default class Player extends Module { this.css_tweaks.toggleHide('player-ext', val === 2); } + updateCaptionsCSS() { // Font const font_size = this.settings.get('player.captions.font-size'); @@ -359,14 +626,14 @@ export default class Player extends Module { font_out.push(`font-size: ${STYLE_VALIDATOR.style.fontSize} !important;`); if ( font_out.length ) - this.css_tweaks.set('captions-font', `.player-captions { + this.css_tweaks.set('captions-font', `.player-captions-container__caption-line { ${font_out.join('\n\t')} }`) else this.css_tweaks.delete('captions-font'); // Position - const enabled = this.settings.get('player.captions.custom-position'), + /*const enabled = this.settings.get('player.captions.custom-position'), vertical = this.settings.get('player.captions.vertical'), horizontal = this.settings.get('player.captions.horizontal'), alignment = this.settings.get('player.captions.alignment'); @@ -376,7 +643,7 @@ export default class Player extends Module { return; } - const out = [], + const out = [], align_out = [], align_horizontal = alignment % 10, align_vertical = Math.floor(alignment / 10); @@ -394,6 +661,11 @@ export default class Player extends Module { STYLE_VALIDATOR.style.top = ''; STYLE_VALIDATOR.style.top = horizontal; if ( STYLE_VALIDATOR.style.top ) { + if ( align_horizontal === 1 ) + align_out.push(`align-items: flex-start !important;`); + else if ( align_horizontal === 3 ) + align_out.push(`align-items: flex-end !important;`); + out.push(`${align_horizontal === 3 ? 'right' : 'left'}: ${STYLE_VALIDATOR.style.top} !important;`); out.push(`${align_horizontal === 3 ? 'left' : 'right'}: unset !important;`); custom_left = true; @@ -406,74 +678,11 @@ export default class Player extends Module { this.css_tweaks.set('captions-position', `.player-captions-container { ${out.join('\n\t')}; -}`); +}${align_out.length ? `.player-captions-container__caption-window { + ${align_out.join('\n\t')} +}` : ''}`);*/ } - onEnable() { - 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('theatre-no-whispers', this.settings.get('player.theatre.no-whispers')); - this.css_tweaks.toggle('theatre-metadata', this.settings.get('player.theatre.metadata')); - this.css_tweaks.toggle('player-hide-mouse', this.settings.get('player.hide-mouse')); - this.css_tweaks.toggleHide('player-event-bar', this.settings.get('player.hide-event-bar')); - this.css_tweaks.toggleHide('player-rerun-bar', this.settings.get('player.hide-rerun-bar')); - this.updateHideExtensions(); - this.updateCaptionsCSS(); - this.installVisibilityHook(); - - const t = this; - - this.SquadStreamBar.ready(cls => { - const old_should_render = cls.prototype.shouldRenderSquadBanner; - - cls.prototype.shouldRenderSquadBanner = function(...args) { - if ( t.settings.get('player.hide-squad-banner') ) - return false; - - return old_should_render.call(this, ...args); - } - - this.SquadStreamBar.forceUpdate(); - this.updateSquadContext(); - }); - - this.SquadStreamBar.on('mount', this.updateSquadContext, this); - this.SquadStreamBar.on('update', this.updateSquadContext, this); - this.SquadStreamBar.on('unmount', this.updateSquadContext, this); - - this.Player.on('mount', this.onMount, this); - this.Player.on('unmount', this.onUnmount, this); - - this.Player.ready((cls, instances) => { - const old_init = cls.prototype.initializePlayer; - - if ( old_init ) { - cls.prototype.initializePlayer = function(...args) { - const ret = old_init.call(this, ...args); - t.process(this); - return ret; - } - - } else { - this.Player.on('will-mount', this.overrideInitialize, this); - } - - for(const inst of instances) { - if ( ! old_init ) - this.overrideInitialize(inst); - - this.onMount(inst); - this.process(inst); - } - }); - - this.on('i18n:update', () => { - for(const inst of this.Player.instances) { - this.addPiPButton(inst); - this.addResetButton(inst); - } - }); - } installVisibilityHook() { if ( ! document.pictureInPictureEnabled ) { @@ -500,290 +709,14 @@ export default class Player extends Module { } - updateSquadContext() { - this.settings.updateContext({ - squad_bar: this.hasSquadBar() - }); - } - - - hasSquadBar() { - const inst = this.SquadStreamBar.first; - return inst ? inst.shouldRenderSquadBanner(inst.props) : false - } - - - overrideInitialize(inst) { - const t = this, - old_init = inst.initializePlayer; - - inst.initializePlayer = function(...args) { - const ret = old_init.call(inst, ...args); - t.process(inst); - return ret; - } - } - - - onMount(inst) { - if ( this.settings.get('player.theatre.auto-enter') && inst.onTheatreChange ) - inst.onTheatreChange(true); - - if ( this.settings.get('player.no-autoplay') || (! this.settings.get('player.home.autoplay') && this.router.current.name === 'front-page') ) { - if ( inst.player ) - this.disableAutoplay(inst); - else { - const wrapped = inst.onPlayerReady; - inst.onPlayerReady = () => { - const ret = wrapped.call(inst); - this.disableAutoplay(inst); - return ret; - } - } - } - } - - - onUnmount(inst) { // eslint-disable-line class-methods-use-this - this.cleanup(inst); - } - - - process(inst) { - this.addResetButton(inst); + updateGUI(inst) { this.addPiPButton(inst); - this.addEndedListener(inst); - this.addStateTags(inst); - this.addControlVisibility(inst); - this.updateVolumeScroll(inst); - } - - - cleanup(inst) { // eslint-disable-line class-methods-use-this - const p = inst.player, - pr = inst.playerRef, - video = pr && pr.querySelector('video'), - reset = pr && pr.querySelector('.ffz--player-reset'), - pip = pr && pr.querySelector('.ffz--player-pip'); - - if ( reset ) - reset.remove(); - - if ( pip ) - pip.remove(); - - if ( video && video._ffz_pip_enter ) { - video.removeEventListener('enterpictureinpicture', video._ffz_pip_enter); - video.removeEventListener('leavepictureinpicture', video._ffz_pip_exit); - video._ffz_pip_enter = null; - video._ffz_pip_exit = null; - } - - if ( inst._ffz_on_ended ) { - p && off(p, 'ended', inst._ffz_on_ended); - inst._ffz_on_ended = null; - } - - if ( inst._ffz_visibility_handler ) { - if ( pr ) { - off(pr, 'mousemove', inst._ffz_visibility_handler); - off(pr, 'mouseleave', inst._ffz_visibility_handler); - } - - inst._ffz_visibility_handler = null; - } - - if ( inst._ffz_scroll_handler ) { - pr && off(pr, 'wheel', inst._ffz_scroll_handler); - inst._ffz_scroll_handler = null; - } - - if ( inst._ffz_autoplay_handler ) { - if ( p ) { - off(p, 'play', inst._ffz_autoplay_handler); - off(p, 'playing', inst._ffz_autoplay_handler); - off(p, 'contentShowing', inst._ffz_autoplay_handler); - } - - inst._ffz_autoplay_handler = null; - } - - if ( inst._ffz_on_state ) { - if ( p ) { - off(p, 'ended', inst._ffz_on_state); - off(p, 'pause', inst._ffz_on_state); - off(p, 'playing', inst._ffz_on_state); - off(p, 'error', inst._ffz_on_state); - } - - inst._ffz_on_state = null; - } - } - - - addEndedListener(inst) { - let p = inst.player; - if ( ! p ) - return; - - if ( p.player ) - p = p.player; - - if ( inst._ffz_on_ended ) - off(p, 'ended', inst._ffz_on_ended); - - on(p, 'ended', inst._ffz_on_ended = async () => { - if ( this.settings.get('player.vod.autoplay') ) - return; - - try { - (await this.parent.awaitElement('.pl-rec__cancel', inst.playerRef, 1000)).click(); - } catch(err) { /* do nothing~ */ } - }); - } - - - addControlVisibility(inst) { // eslint-disable-line class-methods-use-this - const p = inst.playerRef; - if ( ! p ) - return; - - if ( inst._ffz_visibility_handler ) { - off(p, 'mousemove', inst._ffz_visibility_handler); - off(p, 'mouseleave', inst._ffz_visibility_handler); - } - - let timer; - - const c = () => { p.dataset.controls = false }; - const f = inst._ffz_visibility_handler = e => { - clearTimeout(timer); - if ( e.type === 'mouseleave' ) - return c(); - - timer = setTimeout(c, 5000); - p.dataset.controls = true; - }; - - on(p, 'mousemove', f); - on(p, 'mouseleave', f); - } - - - addStateTags(inst) { - let p = inst.player; - if ( ! p ) - return; - - if ( p.player ) - p = p.player; - - if ( inst._ffz_on_state ) { - off(p, 'ended', inst._ffz_on_state); - off(p, 'pause', inst._ffz_on_state); - off(p, 'playing', inst._ffz_on_state); - off(p, 'error', inst._ffz_on_state); - } - - const f = inst._ffz_on_state = () => this.updateStateTags(inst); - - on(p, 'ended', f); - on(p, 'pause', f); - on(p, 'playing', f); - on(p, 'error', f); - - f(); - } - - - updateStateTags(inst) { // eslint-disable-line class-methods-use-this - const p = inst.playerRef; - let player = inst.player; - if ( ! p || ! player ) - return; - - if ( player.player ) - player = player.player; - - p.dataset.ended = player.ended; - p.dataset.paused = player.paused; - } - - - disableAutoplay(inst) { - let p = inst.player; - if ( ! p ) - return this.log.warn('disableAutoplay() called without Player'); - - if ( p.player ) - p = p.player; - - if ( p.readyState > 0 ) { - this.log.info('Player already playing. Pausing.'); - return p.pause(); - } - - if ( ! inst._ffz_autoplay_handler ) { - const listener = inst._ffz_autoplay_handler = () => { - setTimeout(() => { - this.log.info('Pausing due to playback.'); - inst._ffz_autoplay_handler = null; - p.pause(); - - setTimeout(() => { - off(p, 'play', listener); - off(p, 'playing', listener); - off(p, 'contentShowing', listener); - }, 250); - }); - } - - on(p, 'play', listener); - on(p, 'playing', listener); - on(p, 'contentShowing', listener); - } - } - - - updateVolumeScroll(inst, enabled) { - if ( enabled === undefined ) - enabled = this.settings.get('player.volume-scroll'); - - const pr = inst.playerRef; - if ( ! pr ) - return; - - if ( ! enabled && inst._ffz_scroll_handler ) { - off(pr, 'wheel', inst._ffz_scroll_handler); - inst._ffz_scroll_handler = null; - - } else if ( enabled && ! inst._ffz_scroll_handler ) { - on(pr, 'wheel', inst._ffz_scroll_handler = e => { - const delta = e.wheelDelta || -(e.deltaY || e.detail || 0); - let player = inst.player; - if ( player.player ) - player = player.player; - - if ( player ) { - const amount = this.settings.get('player.volume-scroll-steps'), - volume = Math.max(0, Math.min(1, player.getVolume() + (delta > 0 ? amount : -amount))); - - player.setVolume(volume); - if ( volume !== 0 ) - player.setMuted(false); - } - - e.preventDefault(); - return false; - }); - } } addPiPButton(inst, tries = 0) { - const t = this, - el = inst.playerRef && inst.playerRef.querySelector('.player-buttons-right .pl-flex'), - container = el && el.parentElement, + const outer = inst.props.containerRef || this.fine.getChildNode(inst), + container = outer && outer.querySelector('.player-controls__right-control-group'), has_pip = document.pictureInPictureEnabled && this.settings.get('player.button.pip'); if ( ! container ) { @@ -791,42 +724,54 @@ export default class Player extends Module { return; if ( tries < 5 ) - return setTimeout(this.addPiPButton.bind(this, inst, (tries||0) + 1), 250); + return setTimeout(this.addPiPButton.bind(this, inst, (tries || 0) + 1), 250); - return this.log.warn('Unable to find container element for PiP Button'); + return this.log.warn('Unable to find container element for PiP button.'); } - let tip, btn = container.querySelector('.ffz--player-pip'); + let icon, tip, btn, cont = container.querySelector('.ffz--player-pip'); if ( ! has_pip ) { - if ( btn ) - btn.remove(); + if ( cont ) + cont.remove(); return; } - if ( ! btn ) { - btn = (); + if ( ! cont ) { + cont = (
+ {btn = ()} + {tip = (); - container.insertBefore(btn, el.nextSibling); + container.appendChild(cont); - } else - tip = btn.querySelector('.player-tip'); + } else { + icon = cont.querySelector('figure'); + btn = cont.querySelector('button'); + tip = cont.querySelector('.tw-tooltip'); + } const pip_active = !!document.pictureInPictureElement - btn.classList.toggle('ffz-i-window-restore', ! pip_active); - btn.classList.toggle('ffz-i-window-maximize', pip_active); + icon.classList.toggle('ffz-i-window-restore', ! pip_active); + icon.classList.toggle('ffz-i-window-maximize', pip_active); - tip.dataset.tip = this.i18n.t('player.pip_button', 'Click to Toggle Picture-in-Picture'); + btn.setAttribute('aria-label', tip.textContent = this.i18n.t('player.pip_button', 'Click to Toggle Picture-in-Picture')); } + pipPlayer(inst) { - const video = inst.playerRef && inst.playerRef.querySelector('video'); + const video = inst.props.mediaPlayerInstance?.mediaSinkManager?.video; if ( ! video || ! document.pictureInPictureEnabled ) return; @@ -847,84 +792,51 @@ export default class Player extends Module { } - addResetButton(inst, tries = 0) { - const t = this, - el = inst.playerRef && inst.playerRef.querySelector('.player-buttons-right .pl-flex'), - container = el && el.parentElement; - - if ( ! this.settings.get('player.button.reset') ) { - if ( container ) { - const btn = container.querySelector('.ffz--player-reset'); - if ( btn ) - btn.remove(); - } - + tryTheatreMode(inst) { + if ( ! this.settings.get('player.theatre.auto-enter') ) return; + + if ( inst?.props?.onTheatreModeEnabled ) + inst.props.onTheatreModeEnabled(); + } + + + /** + * Tries to reposition the player, using a method exposed on the + * Squad Streaming bar. + * + * @memberof Player + * @returns {void} + */ + repositionPlayer() { + for(const inst of this.SquadStreamBar.instances) { + if ( inst?.props?.triggerPlayerReposition ) { + inst.props.triggerPlayerReposition(); + return; + } } - - if ( ! container ) { - if ( tries < 5 ) - return setTimeout(this.addResetButton.bind(this, inst, (tries||0) + 1), 250); - - return this.log.warn('Unable to find container element for Reset Button'); - } - - let tip = container.querySelector('.ffz--player-reset .player-tip'); - - if ( ! tip ) - container.insertBefore(, el.nextSibling); - - tip.dataset.tip = this.i18n.t('player.reset_button', 'Double-Click to Reset Player'); } - - resetPlayer(inst) { - // Player shutdown logic copied from componentWillUnmount - inst.checkPlayerDependencyAnimationFrame && cancelAnimationFrame(inst.checkPlayerDependencyAnimationFrame); - inst.checkPlayerDependencyAnimationFrame = null; - inst.maybeDetachFromWindow(); - - const ES = this.web_munch.getModule('extension-service'); - if ( ES ) - ES.extensionService.unregisterPlayer(); - - this.cleanup(inst); - - const klass = inst.player.constructor; - - inst.player.destroy(); - inst.playerRef.innerHTML = ''; - - inst.initializePlayer(klass); + updateSquadContext() { + this.settings.updateContext({ + squad_bar: this.hasSquadBar + }); } - - getInternalPlayer(inst) { - if ( ! inst ) - inst = this.Player.first; - - const node = this.fine.getChildNode(inst), - el = node && node.querySelector('.player-ui'); - - if ( ! el || ! el._reactRootContainer ) - return null; - - return this.fine.searchTree(el, n => n.props && n.props.player && n.context && n.context.store); + get hasSquadBar() { + const inst = this.SquadStreamBar.first; + return inst ? inst.shouldRenderSquadBanner(inst.props) : false } + get playerUI() { + const container = this.fine.searchTree(this.Player.first, n => n.props && n.props.uiContext, 150); + return container?.props?.uiContext; + } get current() { - // There should only ever be one player instance, but might change - // when they re-add support for the mini player. for(const inst of this.Player.instances) - if ( inst && inst.player ) - return inst.player; + if ( inst?.props?.mediaPlayerInstance ) + return inst.props.mediaPlayerInstance; return null; } diff --git a/src/utilities/compat/fine.js b/src/utilities/compat/fine.js index ad5fe5f6..36fe350a 100644 --- a/src/utilities/compat/fine.js +++ b/src/utilities/compat/fine.js @@ -77,8 +77,8 @@ export default class Fine extends Module { return instance.return; } - getParentNode(instance) { - if ( instance._reactInternalFiber ) + getParentNode(instance, max_depth = 100, traverse_roots = false) { + /*if ( instance._reactInternalFiber ) instance = instance._reactInternalFiber; else if ( instance instanceof Node ) instance = this.getReactInstance(instance); @@ -87,11 +87,13 @@ export default class Fine extends Module { if ( instance.stateNode instanceof Node ) return instance.stateNode else - instance = instance.parent; + instance = instance.parent;*/ + + return this.searchParent(instance, n => n instanceof Node, max_depth, 0, traverse_roots); } - getChildNode(instance, max_depth = 100) { - if ( instance._reactInternalFiber ) + getChildNode(instance, max_depth = 100, traverse_roots = false) { + /*if ( instance._reactInternalFiber ) instance = instance._reactInternalFiber; else if ( instance instanceof Node ) instance = this.getReactInstance(instance); @@ -104,7 +106,9 @@ export default class Fine extends Module { if ( max_depth < 0 ) return null; instance = instance.child; - } + }*/ + + return this.searchTree(instance, n => n instanceof Node, max_depth, 0, traverse_roots); } getHostNode(instance, max_depth = 100) { @@ -243,6 +247,25 @@ export default class Fine extends Module { } + findAllMatching(node, criteria, max_depth=15, parents=false, depth=0, traverse_roots=true) { + const matches = new Set, + crit = n => ! matches.has(n) && criteria(n); + + while(true) { + const match = parents ? + this.searchParent(node, crit, max_depth, depth, traverse_roots) : + this.searchTree(node, crit, max_depth, depth, traverse_roots); + + if ( ! match ) + break; + + matches.add(match); + } + + return matches; + } + + searchAll(node, criterias, max_depth=15, depth=0, data, traverse_roots = true) { if ( ! node ) node = this.react;