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;