2021-02-26 15:35:26 -05:00
'use strict' ;
// ============================================================================
// Twitch Player -- Shared Code
// ============================================================================
import Module from 'utilities/module' ;
import { createElement , on , off } from 'utilities/dom' ;
2021-05-29 18:14:29 -04:00
import { isValidShortcut , debounce , has } from 'utilities/object' ;
2021-09-06 16:48:48 -04:00
import { IS _FIREFOX } from 'utilities/constants' ;
import { getFontsList , useFont } from 'utilities/fonts' ;
2021-02-26 15:35:26 -05:00
const STYLE _VALIDATOR = createElement ( 'span' ) ;
2022-08-02 16:59:50 -04:00
const LEFT _CONTROLS = '.video-player__default-player .player-controls__left-control-group' ;
const RIGHT _CONTROLS = '.video-player__default-player .player-controls__right-control-group' ;
2021-04-24 14:37:01 -04:00
const HAS _COMPRESSOR = window . AudioContext && window . DynamicsCompressorNode != null ,
HAS _GAIN = HAS _COMPRESSOR && window . GainNode != null ;
const SCROLL _I18N = 'setting.entry.player.volume-scroll.values' ,
SCROLL _OPTIONS = [
{ value : false , title : 'Disabled' , i18n _key : ` ${ SCROLL _I18N } .false ` } ,
{ value : true , title : 'Enabled' , i18n _key : ` ${ SCROLL _I18N } .true ` } ,
{ value : 2 , title : 'Enabled with Right-Click' , i18n _key : ` ${ SCROLL _I18N } .2 ` } ,
{ value : 3 , title : 'Enabled with Alt' , i18n _key : ` ${ SCROLL _I18N } .3 ` } ,
{ value : 4 , title : 'Enabled with Alt + Right-Click' , i18n _key : ` ${ SCROLL _I18N } .4 ` } ,
{ value : 5 , title : 'Enabled with Shift' , i18n _key : ` ${ SCROLL _I18N } .5 ` } ,
{ value : 6 , title : 'Enabled with Shift + Right-Click' , i18n _key : ` ${ SCROLL _I18N } .6 ` } ,
{ value : 7 , title : 'Enabled with Ctrl' , i18n _key : ` ${ SCROLL _I18N } .7 ` } ,
{ value : 8 , title : 'Enabled with Ctrl + Right-Click' , i18n _key : ` ${ SCROLL _I18N } .8 ` }
] ;
function wantsRMB ( setting ) {
return setting === 2 || setting === 4 || setting === 6 || setting === 8 ;
}
function matchesEvent ( setting , event , has _rmb ) {
if ( wantsRMB ( setting ) && event . button !== 2 && ! has _rmb )
return false ;
if ( ! event . altKey && ( setting === 3 || setting === 4 ) )
return false ;
if ( ! event . shiftKey && ( setting === 5 || setting === 6 ) )
return false ;
if ( ! event . ctrlKey && ( setting === 7 || setting === 8 ) )
return false ;
return true ;
}
2021-02-26 15:35:26 -05:00
function rotateButton ( event ) {
const target = event . currentTarget ,
icon = target && target . querySelector ( 'figure' ) ;
if ( ! icon || icon . classList . contains ( 'ffz-i-t-reset-clicked' ) )
return ;
icon . classList . toggle ( 'ffz-i-t-reset' , false ) ;
icon . classList . toggle ( 'ffz-i-t-reset-clicked' , true ) ;
setTimeout ( ( ) => {
icon . classList . toggle ( 'ffz-i-t-reset' , true ) ;
icon . classList . toggle ( 'ffz-i-t-reset-clicked' , false ) ;
} , 500 ) ;
}
export default class PlayerBase extends Module {
constructor ( ... args ) {
super ( ... args ) ;
this . inject ( 'i18n' ) ;
this . inject ( 'settings' ) ;
this . inject ( 'site.fine' ) ;
this . inject ( 'site.css_tweaks' ) ;
2021-04-24 14:37:01 -04:00
this . onShortcut = this . onShortcut . bind ( this ) ;
2022-08-02 16:59:50 -04:00
this . LEFT _CONTROLS = LEFT _CONTROLS ;
this . RIGHT _CONTROLS = RIGHT _CONTROLS ;
2021-02-26 15:35:26 -05:00
this . registerSettings ( ) ;
}
registerSettings ( ) {
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'
}
} ) ;
2022-12-07 16:52:07 -05:00
this . settings . add ( 'player.fade-pause-buffer' , {
default : false ,
ui : {
path : 'Player > General >> Playback' ,
title : 'Fade the player when paused or buffering to make the UI easier to see.' ,
component : 'setting-check-box'
} ,
changed : val => this . css _tweaks . toggle ( 'player-fade-paused' , val )
} ) ;
2021-02-26 15:35:26 -05:00
if ( HAS _COMPRESSOR ) {
this . settings . add ( 'player.compressor.enable' , {
default : true ,
ui : {
path : 'Player > Compressor @{"description": "These settings control optional dynamic range compression for the player, a form of audio processing that reduces the volume of loud sounds and amplifies quiet sounds, thus normalizing or compressing the volume. This uses a [DynamicsCompressorNode](https://developer.mozilla.org/en-US/docs/Web/API/DynamicsCompressorNode) from the Web Audio API behind the scenes if you want to learn more."} >> General' ,
title : 'Enable the audio compressor and add an `Audio Compressor` button to the player controls.' ,
sort : - 1000 ,
component : 'setting-check-box'
} ,
changed : ( ) => {
for ( const inst of this . Player . instances )
this . addCompressorButton ( inst ) ;
}
} ) ;
this . settings . add ( 'player.compressor.default' , {
default : false ,
ui : {
path : 'Player > Compressor >> General' ,
title : 'Enable the compressor by default.' ,
component : 'setting-check-box'
} ,
changed : ( ) => {
for ( const inst of this . Player . instances )
this . compressPlayer ( inst ) ;
}
} ) ;
2022-05-05 13:41:49 -04:00
this . settings . add ( 'player.compressor.force-legacy' , {
default : false ,
ui : {
path : 'Player > Compressor >> Advanced' ,
title : 'Force use of legacy browser API.' ,
description : 'This setting forces FrankerFaceZ to attempt to use an older browser API to create the compressor. Please reset your player after changing this setting.' ,
component : 'setting-check-box' ,
force _seen : true
}
} ) ;
2021-04-24 14:37:01 -04:00
this . settings . add ( 'player.compressor.shortcut' , {
default : null ,
requires : [ 'player.compressor.enable' ] ,
process ( ctx , val ) {
if ( ! ctx . get ( 'player.compressor.enable' ) )
return null ;
return val ;
} ,
ui : {
path : 'Player > Compressor >> General' ,
title : 'Shortcut Key' ,
description : 'This key sequence can be used to toggle the compressor.' ,
component : 'setting-hotkey'
} ,
changed : ( ) => {
this . updateShortcut ( ) ;
for ( const inst of this . Player . instances )
this . addCompressorButton ( inst ) ;
}
} ) ;
if ( HAS _GAIN ) {
this . settings . add ( 'player.gain.enable' , {
default : false ,
ui : {
sort : - 1 ,
path : 'Player > Compressor >> Gain Control @{"sort": 50, "description": "Gain Control gives you extra control over the output volume when using the Compressor by letting you adjust the volume after the compressor runs, while the built-in volume slider takes affect before the compressor. This uses a simple [GainNode](https://developer.mozilla.org/en-US/docs/Web/API/GainNode) from the Web Audio API, connected in sequence after the DynamicsCompressorNode the Compressor uses."}' ,
title : 'Enable gain control when the audio compressor is enabled.' ,
component : 'setting-check-box'
} ,
changed : ( ) => {
for ( const inst of this . Player . instances )
this . compressPlayer ( inst ) ;
}
} ) ;
this . settings . add ( 'player.gain.no-volume' , {
default : false ,
requires : [ 'player.gain.enable' ] ,
process ( ctx , val ) {
if ( ! ctx . get ( 'player.gain.enable' ) )
return false ;
return val ;
} ,
ui : {
path : 'Player > Compressor >> Gain Control' ,
title : 'Force built-in volume to 100% when the audio compressor is enabled.' ,
description : 'With this enabled, the built-in volume will be hidden and the Gain Control will be the only way to change volume.' ,
component : 'setting-check-box'
} ,
changed : val => {
this . css _tweaks . toggleHide ( 'player-gain-volume' , val ) ;
for ( const inst of this . Player . instances )
this . updateGainVolume ( inst ) ;
}
} ) ;
this . settings . add ( 'player.gain.scroll' , {
default : false ,
ui : {
path : 'Player > Compressor >> Gain Control' ,
title : 'Scroll Adjust' ,
description : 'Adjust the gain by scrolling with the mouse wheel. This setting takes precedence over adjusting the volume by scrolling. *This setting will not work properly on streams with visible extensions when mouse interaction with extensions is allowed.*' ,
component : 'setting-select-box' ,
data : SCROLL _OPTIONS
}
} ) ;
this . settings . add ( 'player.gain.default' , {
default : 100 ,
requires : [ 'player.gain.min' , 'player.gain.max' ] ,
process ( ctx , val ) {
const min = ctx . get ( 'player.gain.min' ) ,
max = ctx . get ( 'player.gain.max' ) ;
val /= 100 ;
if ( val < min )
val = min ;
if ( val > max )
val = max ;
return val ;
} ,
ui : {
path : 'Player > Compressor >> Gain Control' ,
title : 'Default Value' ,
component : 'setting-text-box' ,
description : 'The default value for gain control, when gain control is enabled. 100% means no change in volume.' ,
process : 'to_int' ,
bounds : [ 0 , true ]
} ,
changed : ( ) => this . updateGains ( )
} ) ;
this . settings . add ( 'player.gain.min' , {
default : 0 ,
process : ( ctx , val ) => val / 100 ,
ui : {
path : 'Player > Compressor >> Gain Control' ,
title : 'Minimum' ,
component : 'setting-text-box' ,
description : '**Range:** 0 ~ 100\n\nThe minimum allowed value for gain control. 0% is effectively muted.' ,
process : 'to_int' ,
bounds : [ 0 , true , 100 , true ]
} ,
changed : ( ) => this . updateGains ( )
} ) ;
this . settings . add ( 'player.gain.max' , {
default : 200 ,
process : ( ctx , val ) => val / 100 ,
ui : {
path : 'Player > Compressor >> Gain Control' ,
title : 'Maximum' ,
component : 'setting-text-box' ,
description : '**Range:** 100 ~ 1000\n\nThe maximum allowed value for gain control. 100% is no change. 200% is double the volume.' ,
process : 'to_int' ,
bounds : [ 100 , true , 1000 , true ]
} ,
changed : ( ) => this . updateGains ( )
} ) ;
}
2021-02-26 15:35:26 -05:00
this . settings . add ( 'player.compressor.threshold' , {
default : - 50 ,
ui : {
path : 'Player > Compressor >> Advanced @{"sort": 1000}' ,
title : 'Threshold' ,
sort : 0 ,
description : '**Range:** -100 ~ 0\n\nThe decibel value above which the compression will start taking effect.' ,
component : 'setting-text-box' ,
2021-04-24 14:37:01 -04:00
process : 'to_int' ,
bounds : [ - 100 , true , 0 , true ]
2021-02-26 15:35:26 -05:00
} ,
changed : ( ) => this . updateCompressors ( )
} ) ;
this . settings . add ( 'player.compressor.knee' , {
default : 40 ,
ui : {
path : 'Player > Compressor >> Advanced' ,
title : 'Knee' ,
sort : 5 ,
description : '**Range:** 0 ~ 40\n\nA decibel value representing the range above the threshold where the curve smoothly transitions to the compressed portion.' ,
component : 'setting-text-box' ,
2021-04-24 14:37:01 -04:00
process : 'to_int' ,
bounds : [ 0 , true , 40 , true ]
2021-02-26 15:35:26 -05:00
} ,
changed : ( ) => this . updateCompressors ( )
} ) ;
this . settings . add ( 'player.compressor.ratio' , {
default : 12 ,
ui : {
path : 'Player > Compressor >> Advanced' ,
title : 'Ratio' ,
sort : 10 ,
description : '**Range:** 0 ~ 20\n\nThe amount of change, in dB, needed in the input for a 1 dB change in the output.' ,
component : 'setting-text-box' ,
2021-04-24 14:37:01 -04:00
process : 'to_int' ,
bounds : [ 0 , true , 20 , true ]
2021-02-26 15:35:26 -05:00
} ,
changed : ( ) => this . updateCompressors ( )
} ) ;
this . settings . add ( 'player.compressor.attack' , {
default : 0 ,
ui : {
path : 'Player > Compressor >> Advanced' ,
title : 'Attack' ,
sort : 15 ,
description : '**Range:** 0 ~ 1\n\nThe amount of time, in seconds, required to reduce the gain by 10 dB.' ,
component : 'setting-text-box' ,
2021-04-24 14:37:01 -04:00
process : 'to_float' ,
bounds : [ 0 , true , 1 , true ]
2021-02-26 15:35:26 -05:00
} ,
changed : ( ) => this . updateCompressors ( )
} ) ;
this . settings . add ( 'player.compressor.release' , {
default : 0.25 ,
ui : {
path : 'Player > Compressor >> Advanced' ,
title : 'Release' ,
sort : 20 ,
description : '**Range:** 0 ~ 1\nThe amount of time, in seconds, required to increase the gain by 10 dB.' ,
component : 'setting-text-box' ,
2021-04-24 14:37:01 -04:00
process : 'to_float' ,
bounds : [ 0 , true , 1 , true ]
2021-02-26 15:35:26 -05:00
} ,
changed : ( ) => this . updateCompressors ( )
} ) ;
}
this . settings . add ( 'player.allow-catchup' , {
default : true ,
ui : {
path : 'Player > General @{"sort": -1000} >> General' ,
title : 'Allow the player to speed up to reduce delay.' ,
description : 'Twitch, by default, will apply a minor speed up to live video when you have a large delay to the broadcaster in order to catch back up with the live broadcast. This may result in audio distortion. Disable this to prevent the automatic speed changes.' ,
component : 'setting-check-box'
} ,
changed : ( ) => this . updatePlaybackRates ( )
} ) ;
this . settings . add ( 'player.mute-click' , {
default : false ,
ui : {
path : 'Player > General >> Volume' ,
title : 'Mute or unmute the player by middle-clicking.' ,
component : 'setting-check-box'
}
} ) ;
this . settings . add ( 'player.volume-scroll' , {
default : false ,
ui : {
path : 'Player > General >> Volume' ,
title : 'Adjust volume by scrolling with the mouse wheel.' ,
description : '*This setting will not work properly on streams with visible extensions when mouse interaction with extensions is allowed.*' ,
component : 'setting-select-box' ,
2021-04-24 14:37:01 -04:00
data : SCROLL _OPTIONS
2021-02-26 15:35:26 -05:00
}
} ) ;
this . settings . add ( 'player.button.reset' , {
default : true ,
ui : {
path : 'Player > General >> General' ,
title : 'Add a `Reset Player` button to the player controls.' ,
description : "Double-clicking the Reset Player button attempts to reset the Twitch player's internal state, fixing playback issues without a full page refresh." ,
component : 'setting-check-box'
} ,
changed : ( ) => {
for ( const inst of this . Player . instances )
this . addResetButton ( inst ) ;
}
} ) ;
if ( document . pictureInPictureEnabled )
this . settings . add ( 'player.button.pip' , {
default : true ,
ui : {
path : 'Player > General >> General' ,
title : 'Add a `Picture-in-Picture` button to the player controls.' ,
description : "Clicking the PiP button attempts to toggle Picture-in-Picture mode for the player's video." ,
component : 'setting-check-box'
} ,
changed : ( ) => {
for ( const inst of this . Player . instances )
this . addPiPButton ( inst ) ;
}
} ) ;
this . settings . add ( 'player.volume-scroll-steps' , {
default : 0.1 ,
ui : {
path : 'Player > General >> Volume' ,
title : 'Volume scroll amount' ,
description : 'How much the volume level is changed per individual scroll input.' ,
component : 'setting-select-box' ,
data : [
{ value : 0.1 , title : '10%' } ,
{ value : 0.05 , title : '5%' } ,
{ value : 0.02 , title : '2%' } ,
{ value : 0.01 , title : '1%' }
]
}
} ) ;
this . settings . add ( 'player.captions.font-size' , {
default : '' ,
ui : {
path : 'Player > Closed Captioning >> Font' ,
title : 'Font Size' ,
description : 'How large should captions be. This can be a percentage, such as `10%`, or a pixel value, such as `50px`.' ,
component : 'setting-text-box'
} ,
changed : ( ) => this . updateCaptionsCSS ( )
} ) ;
this . settings . add ( 'player.captions.font-family' , {
default : '' ,
ui : {
path : 'Player > Closed Captioning >> Font' ,
title : 'Font Family' ,
description : 'Override the font used for displaying Closed Captions.' ,
2021-09-06 16:48:48 -04:00
component : 'setting-combo-box' ,
data : ( ) => getFontsList ( )
2021-02-26 15:35:26 -05:00
} ,
changed : ( ) => this . updateCaptionsCSS ( )
} ) ;
/ * t h i s . s e t t i n g s . a d d ( ' p l a y e r . c a p t i o n s . c u s t o m - p o s i t i o n ' , {
default : false ,
ui : {
path : 'Player > Closed Captioning >> Position' ,
sort : - 1 ,
title : 'Enable overriding the position and alignment of closed captions.' ,
component : 'setting-check-box'
} ,
changed : ( ) => this . updateCaptionsCSS ( )
} ) ;
this . settings . add ( 'player.captions.vertical' , {
default : '10%' ,
ui : {
path : 'Player > Closed Captioning >> Position' ,
title : 'Vertical Position' ,
component : 'setting-text-box' ,
description : 'Override the position for Closed Captions. This can be a percentage, such as `10%`, or a pixel value, such as `50px`.'
} ,
changed : ( ) => this . updateCaptionsCSS ( )
} ) ;
this . settings . add ( 'player.captions.horizontal' , {
default : '50%' ,
ui : {
path : 'Player > Closed Captioning >> Position' ,
title : 'Horizontal Position' ,
component : 'setting-text-box' ,
description : 'Override the position for Closed Captions. This can be a percentage, such as `10%`, or a pixel value, such as `50px`.'
} ,
changed : ( ) => this . updateCaptionsCSS ( )
} ) ;
this . settings . add ( 'player.captions.alignment' , {
default : 32 ,
ui : {
path : 'Player > Closed Captioning >> Position' ,
title : 'Alignment' ,
component : 'setting-select-box' ,
data : [
{ value : 11 , title : 'Top Left' } ,
{ value : 12 , title : 'Top Center' } ,
{ value : 13 , title : 'Top Right' } ,
{ value : 21 , title : 'Middle Left' } ,
{ value : 22 , title : 'Middle Center' } ,
{ value : 23 , title : 'Middle Right' } ,
{ value : 31 , title : 'Bottom Left' } ,
{ value : 32 , title : 'Bottom Center' } ,
{ value : 33 , title : 'Bottom Right' }
]
} ,
changed : ( ) => this . updateCaptionsCSS ( )
} ) ; * /
this . settings . add ( 'player.ext-hide' , {
default : 0 ,
ui : {
path : 'Player > General >> Extensions' ,
title : 'Show Overlay Extensions' ,
2021-09-04 20:14:58 -04:00
description : 'Note: This feature does not prevent extensions from loading. Hidden extensions are merely invisible. Hiding extensions with this feature will not improve your security.' ,
2021-02-26 15:35:26 -05:00
component : 'setting-select-box' ,
data : [
{ value : 2 , title : 'Never' } ,
{ value : 1 , title : 'With Controls' } ,
{ value : 0 , title : 'Always' }
]
} ,
changed : val => this . updateHideExtensions ( val )
} ) ;
this . settings . add ( 'player.ext-interaction' , {
default : true ,
ui : {
path : 'Player > General >> Extensions' ,
title : 'Allow mouse interaction with overlay extensions.' ,
component : 'setting-check-box'
} ,
changed : val => this . css _tweaks . toggle ( 'player-ext-mouse' , ! val )
} )
this . settings . add ( 'player.no-autoplay' , {
default : false ,
ui : {
path : 'Player > General >> Playback' ,
title : 'Do not automatically start playing videos or streams.' ,
description : 'Note: This feature does not apply when navigating directly from channel to channel.' ,
component : 'setting-check-box'
}
} ) ;
this . settings . add ( 'player.vod.autoplay' , {
default : true ,
ui : {
path : 'Player > General >> Playback' ,
title : 'Auto-play the next recommended video after a video finishes.' ,
component : 'setting-check-box'
}
} ) ;
this . settings . add ( 'player.volume-always-shown' , {
default : false ,
ui : {
path : 'Player > General >> Volume' ,
title : 'Keep the volume slider expanded at all times.' ,
component : 'setting-check-box'
} ,
changed : val => this . css _tweaks . toggle ( 'player-volume' , val )
} ) ;
this . settings . add ( 'player.hide-mouse' , {
default : true ,
ui : {
path : 'Player > General >> General' ,
title : "Hide mouse when controls aren't visible." ,
component : 'setting-check-box'
} ,
changed : val => this . css _tweaks . toggle ( 'player-hide-mouse' , val )
} ) ;
2022-10-07 15:12:15 -04:00
this . settings . add ( 'player.single-click-pause' , {
default : false ,
ui : {
path : 'Player > General >> Playback' ,
title : "Pause/Unpause the player by clicking." ,
component : 'setting-check-box'
}
} ) ;
2021-02-26 15:35:26 -05:00
}
async onEnable ( ) {
await this . settings . awaitProvider ( ) ;
await this . settings . provider . awaitReady ( ) ;
2021-04-24 14:37:01 -04:00
this . css _tweaks . toggleHide ( 'player-gain-volume' , this . settings . get ( 'player.gain.no-volume' ) ) ;
2021-02-26 15:35:26 -05:00
this . css _tweaks . toggle ( 'player-volume' , this . settings . get ( 'player.volume-always-shown' ) ) ;
this . css _tweaks . toggle ( 'player-ext-mouse' , ! this . settings . get ( 'player.ext-interaction' ) ) ;
this . css _tweaks . toggle ( 'player-hide-mouse' , this . settings . get ( 'player.hide-mouse' ) ) ;
2022-12-07 16:52:07 -05:00
this . css _tweaks . toggle ( 'player-fade-paused' , this . settings . get ( 'player.fade-pause-buffer' ) ) ;
2021-02-26 15:35:26 -05:00
this . installVisibilityHook ( ) ;
this . updateHideExtensions ( ) ;
this . updateCaptionsCSS ( ) ;
2021-04-24 14:37:01 -04:00
this . updateShortcut ( ) ;
2021-02-26 15:35:26 -05:00
this . on ( ':reset' , this . resetAllPlayers , this ) ;
this . Player . ready ( ( cls , instances ) => {
this . modifyPlayerClass ( cls ) ;
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 . compressPlayer ( inst ) ;
this . updatePlaybackRate ( inst ) ;
}
} ) ;
this . Player . on ( 'mount' , inst => {
this . updateGUI ( inst ) ;
this . compressPlayer ( inst ) ;
this . updatePlaybackRate ( inst ) ;
} ) ;
this . Player . on ( 'update' , inst => {
this . updateGUI ( inst ) ;
this . compressPlayer ( inst ) ;
this . updatePlaybackRate ( inst ) ;
} ) ;
this . Player . on ( 'unmount' , inst => {
inst . ffzUninstall ( ) ;
} ) ;
this . on ( 'i18n:update' , ( ) => {
for ( const inst of this . Player . instances ) {
this . updateGUI ( inst ) ;
}
} ) ;
}
2021-04-24 14:37:01 -04:00
updateShortcut ( ) {
const Mousetrap = this . Mousetrap = this . Mousetrap || this . resolve ( 'site.web_munch' ) ? . getModule ? . ( 'mousetrap' ) || window . Mousetrap ;
if ( ! Mousetrap || ! Mousetrap . bind )
return ;
if ( this . _shortcut _bound ) {
Mousetrap . unbind ( this . _shortcut _bound ) ;
this . _shortcut _bound = null ;
}
const key = this . settings . get ( 'player.compressor.shortcut' ) ;
if ( HAS _COMPRESSOR && key && isValidShortcut ( key ) ) {
Mousetrap . bind ( key , this . onShortcut ) ;
this . _shortcut _bound = key ;
}
}
onShortcut ( e ) {
for ( const inst of this . Player . instances )
this . compressPlayer ( inst , e ) ;
}
2021-02-26 15:35:26 -05:00
modifyPlayerClass ( cls ) {
const t = this ,
old _attach = cls . prototype . maybeAttachDomEventListeners ;
cls . prototype . ffzInstall = function ( ) {
if ( this . _ffz _installed )
return ;
this . _ffz _installed = true ;
2021-11-05 18:01:28 -04:00
//if ( ! this._ffzUpdateVolume )
// this._ffzUpdateVolume = debounce(this.ffzUpdateVolume.bind(this));
2021-02-26 15:35:26 -05:00
if ( ! this . _ffzUpdateState )
this . _ffzUpdateState = this . ffzUpdateState . bind ( this ) ;
if ( ! this . _ffzErrorReset )
this . _ffzErrorReset = t . addErrorResetButton . bind ( t , this ) ;
if ( ! this . _ffzReady )
this . _ffzReady = this . ffzReady . 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 ) {
2022-12-07 16:52:07 -05:00
on ( events , 'Buffering' , this . _ffzUpdateState ) ;
2021-02-26 15:35:26 -05:00
on ( events , 'Playing' , this . _ffzUpdateState ) ;
on ( events , 'PlayerError' , this . _ffzUpdateState ) ;
on ( events , 'PlayerError' , this . _ffzErrorReset ) ;
on ( events , 'Ended' , this . _ffzUpdateState ) ;
on ( events , 'Ended' , this . ffzOnEnded ) ;
on ( events , 'Ready' , this . _ffzReady ) ;
on ( events , 'Idle' , this . _ffzUpdateState ) ;
}
this . ffzStopAutoplay ( ) ;
}
2021-11-05 18:01:28 -04:00
/ * c l s . p r o t o t y p e . f f z U p d a t e V o l u m e = f u n c t i o n ( ) {
2021-02-26 15:35:26 -05:00
if ( document . hidden )
return ;
const player = this . props . mediaPlayerInstance ,
video = player ? . mediaSinkManager ? . video || player ? . core ? . mediaSinkManager ? . video ;
if ( video ) {
const volume = video . volume ,
muted = player . isMuted ( ) ;
if ( ! video . muted && player . getVolume ( ) !== volume ) {
player . setVolume ( volume ) ;
player . setMuted ( muted ) ;
}
}
2021-11-05 18:01:28 -04:00
} * /
2021-02-26 15:35:26 -05:00
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 , 'PlayerError' , this . _ffzErrorReset ) ;
off ( events , 'Ended' , this . _ffzUpdateState ) ;
off ( events , 'Ended' , this . ffzOnEnded ) ;
off ( events , 'Ready' , this . _ffzReady ) ;
off ( events , 'Idle' , this . _ffzUpdateState ) ;
}
this . ffzRemoveListeners ( ) ;
this . _ffz _state _raf = null ;
this . _ffzUpdateState = null ;
this . _ffzErrorReset = null ;
this . _ffzReady = null ;
this . ffzOnEnded = null ;
}
cls . prototype . ffzReady = function ( ) {
const cont = this . props . containerRef ;
if ( ! cont )
return ;
requestAnimationFrame ( ( ) => {
const icons = cont . querySelectorAll ( '.ffz--player-reset figure' ) ;
for ( const icon of icons ) {
if ( icon . _ffz _unspin )
clearTimeout ( icon . _ffz _unspin ) ;
icon . classList . toggle ( 'loading' , false ) ;
}
} ) ;
}
cls . prototype . ffzStopAutoplay = function ( ) {
if ( t . shouldStopAutoplay ( this ) )
t . stopPlayer ( this . props . mediaPlayerInstance , this . props . playerEvents , this ) ;
}
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 ;
2021-04-14 16:53:15 -04:00
const cont = this . props . containerRef ;
2021-02-26 15:35:26 -05:00
if ( ! cont )
return ;
const ds = cont . dataset ;
ds . controls = this . state ? . active || false ;
2021-04-14 16:53:15 -04:00
let player = this . props . mediaPlayerInstance ;
if ( ! player )
return ;
if ( player . core )
player = player . core ;
2021-04-24 14:37:01 -04:00
const state = player . state ? . state ,
video = player . mediaSinkManager ? . video ;
2021-04-14 16:53:15 -04:00
if ( state === 'Playing' ) {
if ( video ? . _ffz _maybe _compress ) {
video . _ffz _maybe _compress = false ;
t . compressPlayer ( this ) ;
}
}
2021-04-24 14:37:01 -04:00
if ( video && video . _ffz _compressed != null )
ds . compressed = video . _ffz _compressed ;
2021-04-14 16:53:15 -04:00
ds . ended = state === 'Ended' ;
ds . paused = state === 'Idle' ;
2022-12-07 16:52:07 -05:00
ds . buffering = state === 'Buffering' ;
2021-02-26 15:35:26 -05:00
}
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 ) ;
if ( ! this . _ffz _click _handler )
this . _ffz _click _handler = this . ffzClickHandler . bind ( this ) ;
2022-10-07 15:12:15 -04:00
if ( ! this . _ffz _dblclick _handler )
this . _ffz _dblclick _handler = this . ffzDblClickHandler . bind ( this ) ;
2021-02-26 15:35:26 -05:00
if ( ! this . _ffz _menu _handler )
this . _ffz _menu _handler = this . ffzMenuHandler . bind ( this ) ;
on ( cont , 'wheel' , this . _ffz _scroll _handler ) ;
2022-10-07 15:12:15 -04:00
on ( cont , 'dblclick' , this . _ffz _dblclick _handler ) ;
2021-02-26 15:35:26 -05:00
on ( cont , 'mousedown' , this . _ffz _click _handler ) ;
on ( cont , 'contextmenu' , this . _ffz _menu _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 ;
}
if ( this . _ffz _click _handler ) {
off ( cont , 'mousedown' , this . _ffz _click _handler ) ;
this . _ffz _click _handler = null ;
}
if ( this . _ffz _menu _handler ) {
off ( cont , 'contextmenu' , this . _ffz _menu _handler ) ;
this . _ffz _menu _handler = null ;
}
2022-10-07 15:12:15 -04:00
if ( this . _ffz _dblclick _handler ) {
off ( cont , 'dblclick' , this . _ffz _dblclick _handler ) ;
this . _ffz _dblclick _handler = null ;
}
2021-02-26 15:35:26 -05:00
this . _ffz _listeners = false ;
}
2022-10-07 15:12:15 -04:00
cls . prototype . ffzDelayPause = function ( ) {
if ( this . _ffz _pause _timer )
clearTimeout ( this . _ffz _pause _timer ) ;
const player = this . props ? . mediaPlayerInstance ;
if ( ! player . isPaused ( ) )
this . _ffz _pause _timer = setTimeout ( ( ) => {
const player = this . props ? . mediaPlayerInstance ;
if ( ! player . isPaused ( ) )
player . pause ( ) ;
} , 500 ) ;
}
cls . prototype . ffzDblClickHandler = function ( event ) {
if ( ! event )
return ;
if ( this . _ffz _pause _timer )
clearTimeout ( this . _ffz _pause _timer ) ;
}
2021-02-26 15:35:26 -05:00
cls . prototype . ffzClickHandler = function ( event ) {
if ( ! event )
return ;
2021-04-24 14:37:01 -04:00
const vol _scroll = t . settings . get ( 'player.volume-scroll' ) ,
gain _scroll = t . settings . get ( 'player.gain.scroll' ) ,
2022-10-07 15:12:15 -04:00
click _pause = t . settings . get ( 'player.single-click-pause' ) ,
2021-04-24 14:37:01 -04:00
wants _rmb = wantsRMB ( vol _scroll ) || wantsRMB ( gain _scroll ) ;
2022-10-07 15:12:15 -04:00
// Left Click
if ( click _pause && event . button === 0 ) {
if ( ! event . target || ! event . target . classList . contains ( 'click-handler' ) )
return ;
this . ffzDelayPause ( ) ;
}
// Right Click
2021-04-24 14:37:01 -04:00
if ( wants _rmb && event . button === 2 ) {
2021-02-26 15:35:26 -05:00
this . ffz _rmb = true ;
this . ffz _scrolled = false ;
}
2022-10-07 15:12:15 -04:00
// Middle Click
2021-02-26 15:35:26 -05:00
if ( ! t . settings . get ( 'player.mute-click' ) || event . button !== 1 )
return ;
const player = this . props ? . mediaPlayerInstance ;
if ( ! player ? . isMuted )
return ;
const muted = ! player . isMuted ( ) ;
player . setMuted ( muted ) ;
localStorage . setItem ( 'video-muted' , JSON . stringify ( { default : muted } ) ) ;
event . preventDefault ( ) ;
return false ;
}
cls . prototype . ffzMenuHandler = function ( event ) {
this . ffz _rmb = false ;
if ( this . ffz _scrolled ) {
event . preventDefault ( ) ;
event . stopPropagation ( ) ;
}
}
cls . prototype . ffzScrollHandler = function ( event ) {
2021-04-24 14:37:01 -04:00
const vol _scroll = t . settings . get ( 'player.volume-scroll' ) ,
gain _scroll = t . settings . get ( 'player.gain.scroll' ) ,
2021-04-28 00:51:24 -04:00
no _vol = t . settings . get ( 'player.gain.no-volume' ) ,
2021-04-24 14:37:01 -04:00
matches _gain = gain _scroll && matchesEvent ( gain _scroll , event , this . ffz _rmb ) ,
2021-04-25 13:11:28 -04:00
matches _vol = vol _scroll && matchesEvent ( vol _scroll , event , this . ffz _rmb ) ;
2021-02-26 15:35:26 -05:00
2021-04-24 14:37:01 -04:00
if ( ! matches _gain && ! matches _vol )
2021-02-26 15:35:26 -05:00
return ;
const delta = event . wheelDelta || - ( event . deltaY || event . detail || 0 ) ,
player = this . props ? . mediaPlayerInstance ,
2021-04-25 13:11:28 -04:00
video = player ? . mediaSinkManager ? . video || player ? . core ? . mediaSinkManager ? . video ,
has _gain = video ? . _ffz _compressed && video ? . _ffz _gain != null ,
doing _gain = has _gain && matches _gain ;
2021-02-26 15:35:26 -05:00
2021-04-25 13:11:28 -04:00
if ( ! player ? . getVolume )
2021-02-26 15:35:26 -05:00
return ;
2021-04-25 13:11:28 -04:00
if ( doing _gain ? wantsRMB ( gain _scroll ) : wantsRMB ( vol _scroll ) )
2021-02-26 15:35:26 -05:00
this . ffz _scrolled = true ;
2021-04-24 14:37:01 -04:00
const amount = t . settings . get ( 'player.volume-scroll-steps' ) ;
2021-04-25 13:11:28 -04:00
if ( doing _gain ) {
2021-04-24 14:37:01 -04:00
let value = video . _ffz _gain _value ;
if ( value == null )
value = t . settings . get ( 'player.gain.default' ) ;
const min = t . settings . get ( 'player.gain.min' ) ,
max = t . settings . get ( 'player.gain.max' ) ;
if ( delta > 0 )
value += amount ;
else
value -= amount ;
if ( value < min )
value = min ;
if ( value > max )
value = max ;
video . _ffz _gain _value = value ;
2021-04-28 00:51:24 -04:00
if ( no _vol && value !== 0 ) {
player . setMuted ( false ) ;
localStorage . setItem ( 'video-muted' , JSON . stringify ( { default : false } ) ) ;
}
2021-04-24 14:37:01 -04:00
t . updateGain ( this ) ;
2021-04-25 13:11:28 -04:00
2021-04-28 00:51:24 -04:00
} else if ( matches _vol && ! ( video . _ffz _compressed && no _vol ) ) {
2021-04-25 13:11:28 -04:00
const old _volume = video ? . volume ? ? player . getVolume ( ) ,
volume = Math . max ( 0 , Math . min ( 1 , old _volume + ( delta > 0 ? amount : - amount ) ) ) ;
player . setVolume ( volume ) ;
localStorage . volume = volume ;
if ( volume !== 0 ) {
player . setMuted ( false ) ;
localStorage . setItem ( 'video-muted' , JSON . stringify ( { default : false } ) ) ;
}
2021-02-26 15:35:26 -05:00
}
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 ) ;
}
}
shouldStopAutoplay ( ) { // eslint-disable-line class-methods-use-this
return false ;
}
installVisibilityHook ( ) {
if ( ! document . pictureInPictureEnabled ) {
this . log . info ( 'Skipping visibility hooks. Picture-in-Picture is not available.' ) ;
return ;
}
2021-08-16 17:51:32 -04:00
const t = this ;
2021-02-26 15:35:26 -05:00
document . addEventListener ( 'fullscreenchange' , ( ) => {
const fs = document . fullscreenElement ,
pip = document . pictureInPictureElement ;
if ( fs && pip && ( fs === pip || fs . contains ( pip ) ) )
document . exitPictureInPicture ( ) ;
// Update the UI since we can't enter PiP from Fullscreen
for ( const inst of this . Player . instances )
this . addPiPButton ( inst ) ;
} ) ;
try {
Object . defineProperty ( document , 'hidden' , {
configurable : true ,
get ( ) {
// If Picture in Picture is active, then we should not
// drop quality. Therefore, we need to trick Twitch
// into thinking the document is still active.
if ( document . pictureInPictureElement != null )
return false ;
2021-08-16 17:51:32 -04:00
if ( t . settings . get ( 'player.force-visible' ) )
2021-08-16 17:23:12 -04:00
return false ;
2021-02-26 15:35:26 -05:00
return document . visibilityState === 'hidden' ;
}
} ) ;
2021-08-16 17:23:12 -04:00
2021-02-26 15:35:26 -05:00
} catch ( err ) {
this . log . warn ( 'Unable to install document visibility hook.' , err ) ;
}
}
stopPlayer ( player , events , inst ) {
2021-04-14 16:53:15 -04:00
if ( player && player . pause && ( player . getState ? . ( ) || player . core ? . getState ? . ( ) ) === 'Playing' )
2021-02-26 15:35:26 -05:00
player . pause ( ) ;
else if ( events && ! events . _ffz _stopping ) {
events . _ffz _stopping = true ;
const immediatePause = ( ) => {
if ( inst . props . mediaPlayerInstance ? . pause ) {
inst . props . mediaPlayerInstance . pause ( ) ;
off ( events , 'Playing' , immediatePause ) ;
events . _ffz _stopping = false ;
}
}
this . log . info ( 'Unable to immediately pause. Listening for playing event.' ) ;
on ( events , 'Playing' , immediatePause ) ;
}
}
updateCaptionsCSS ( ) {
// Font
2021-09-06 16:48:48 -04:00
const font _out = [ ] ;
2021-02-26 15:35:26 -05:00
const font _size = this . settings . get ( 'player.captions.font-size' ) ;
let font _family = this . settings . get ( 'player.captions.font-family' ) ;
2021-09-06 16:48:48 -04:00
if ( font _family && font _family . length ) {
const [ processed , unloader ] = useFont ( font _family ) ;
font _family = processed ;
if ( this . _font _unloader )
this . _font _unloader ( ) ;
this . _font _unloader = unloader ;
2021-02-26 15:35:26 -05:00
2021-09-06 16:48:48 -04:00
if ( font _family . indexOf ( ' ' ) !== - 1 && font _family . indexOf ( ',' ) === - 1 && font _family . indexOf ( '"' ) === - 1 && font _family . indexOf ( "'" ) === - 1 )
font _family = ` " ${ font _family } " ` ;
STYLE _VALIDATOR . style . fontFamily = '' ;
STYLE _VALIDATOR . style . fontFamily = font _family ;
if ( STYLE _VALIDATOR . style . fontFamily )
font _out . push ( ` font-family: ${ STYLE _VALIDATOR . style . fontFamily } !important; ` ) ;
}
STYLE _VALIDATOR . style . fontSize = '' ;
2021-02-26 15:35:26 -05:00
STYLE _VALIDATOR . style . fontSize = font _size ;
if ( STYLE _VALIDATOR . style . fontSize )
font _out . push ( ` font-size: ${ STYLE _VALIDATOR . style . fontSize } !important; ` ) ;
if ( font _out . length )
this . css _tweaks . set ( 'captions-font' , ` .player-captions-container__caption-line {
$ { font _out . join ( '\n\t' ) }
} ` )
else
this . css _tweaks . delete ( 'captions-font' ) ;
// Position
/ * c o n s t e n a b l e d = t h i s . s e t t i n g s . g e t ( ' p l a y e r . c a p t i o n s . c u s t o m - p o s i t i o n ' ) ,
vertical = this . settings . get ( 'player.captions.vertical' ) ,
horizontal = this . settings . get ( 'player.captions.horizontal' ) ,
alignment = this . settings . get ( 'player.captions.alignment' ) ;
if ( ! enabled ) {
this . css _tweaks . delete ( 'captions-position' ) ;
return ;
}
const out = [ ] , align _out = [ ] ,
align _horizontal = alignment % 10 ,
align _vertical = Math . floor ( alignment / 10 ) ;
let custom _top = false ,
custom _left = false ;
STYLE _VALIDATOR . style . top = '' ;
STYLE _VALIDATOR . style . top = vertical ;
if ( STYLE _VALIDATOR . style . top ) {
out . push ( ` ${ align _vertical === 3 ? 'bottom' : 'top' } : ${ STYLE _VALIDATOR . style . top } !important; ` )
out . push ( ` ${ align _vertical === 3 ? 'top' : 'bottom' } : unset !important; ` ) ;
custom _top = true ;
}
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 ;
}
if ( align _horizontal !== 2 )
out . push ( ` width: unset !important; ` ) ;
out . push ( ` transform: translate( ${ ( ! custom _left || align _horizontal === 2 ) ? '-50%' : '0' } , ${ ( ! custom _top || align _vertical === 2 ) ? '-50%' : '0' } ) ` ) ;
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' ) }
} ` : ''} ` ) ; * /
}
updateHideExtensions ( val ) {
if ( val === undefined )
val = this . settings . get ( 'player.ext-hide' ) ;
this . css _tweaks . toggleHide ( 'player-ext-hover' , val === 1 ) ;
this . css _tweaks . toggleHide ( 'player-ext' , val === 2 ) ;
}
updateGUI ( inst ) {
this . addPiPButton ( inst ) ;
this . addResetButton ( inst ) ;
this . addCompressorButton ( inst , false ) ;
2021-04-24 14:37:01 -04:00
this . addGainSlider ( inst , false ) ;
2021-02-26 15:35:26 -05:00
this . addMetadata ( inst ) ;
2021-11-05 18:01:28 -04:00
//if ( inst._ffzUpdateVolume )
// inst._ffzUpdateVolume();
2021-02-26 15:35:26 -05:00
this . emit ( ':update-gui' , inst ) ;
}
2021-05-29 18:14:29 -04:00
areControlsDisabled ( inst ) {
if ( ! inst . _ffz _control _state )
this . findControlState ( inst ) ;
if ( inst . _ffz _control _state )
return inst . _ffz _control _state . props . disableControls ;
return false ;
}
findControlState ( inst ) {
if ( ! inst . _ffz _control _state )
inst . _ffz _control _state = this . fine . searchTree ( inst , n => n . props && has ( n . props , 'disableControls' ) , 200 ) ;
}
2021-04-24 14:37:01 -04:00
addGainSlider ( inst , visible _only , tries = 0 ) {
const outer = inst . props . containerRef || this . fine . getChildNode ( inst ) ,
video = inst . props . mediaPlayerInstance ? . mediaSinkManager ? . video || inst . props . mediaPlayerInstance ? . core ? . mediaSinkManager ? . video ,
2022-08-02 16:59:50 -04:00
container = outer && outer . querySelector ( LEFT _CONTROLS ) ;
2021-04-24 14:37:01 -04:00
let gain = video != null && video . _ffz _compressed && video . _ffz _gain ;
2021-05-29 18:14:29 -04:00
if ( this . areControlsDisabled ( inst ) )
gain = null ;
2021-04-24 14:37:01 -04:00
if ( ! container ) {
if ( video && ! gain )
return ;
if ( tries < 5 )
return setTimeout ( this . addGainSlider . bind ( this , inst , visible _only , ( tries || 0 ) + 1 ) , 250 ) ;
return ;
}
const min = this . settings . get ( 'player.gain.min' ) ,
max = this . settings . get ( 'player.gain.max' ) ;
if ( min >= max || max <= min )
gain = null ;
2021-04-28 00:51:24 -04:00
let tip , tipcont , input , extra , fill , cont = container . querySelector ( '.ffz--player-gain' ) ;
2021-04-24 14:37:01 -04:00
if ( ! gain ) {
if ( cont )
cont . remove ( ) ;
return ;
}
if ( ! cont ) {
const on _change = ( ) => {
let value = input . value / 100 ;
const min = this . settings . get ( 'player.gain.min' ) ,
max = this . settings . get ( 'player.gain.max' ) ;
if ( value < min )
value = min ;
if ( value > max )
value = max ;
2021-04-28 00:51:24 -04:00
if ( value == video . _ffz _gain _value )
return ;
const player = inst . props . mediaPlayerInstance ,
core = player . core || player ;
2021-05-29 18:14:29 -04:00
if ( ! this . areControlsDisabled ( inst ) && value > 0 && this . settings . get ( 'player.gain.no-volume' ) && core ? . isMuted ? . ( ) ) {
2021-04-28 00:51:24 -04:00
core . setMuted ( false ) ;
localStorage . setItem ( 'video-muted' , JSON . stringify ( { default : false } ) ) ;
}
2021-04-24 14:37:01 -04:00
video . _ffz _gain _value = value ;
gain . gain . value = value ;
const range = max - min ,
width = ( value - min ) / range ;
fill . style . width = ` ${ width * 100 } % ` ;
extra . textContent = ` ${ Math . round ( value * 100 ) } % ` ;
} ;
2021-05-17 17:02:23 -04:00
cont = ( < div class = "ffz--player-gain volume-slider__slider-container tw-relative ffz-il-tooltip__container" >
2021-04-24 14:37:01 -04:00
< div class = "tw-align-items-center tw-flex tw-full-height" >
< label class = "tw-hide-accessible" > { this . i18n . t ( 'player.gain.label' , 'Gain Control' ) } < / label >
< div class = "tw-flex tw-full-width tw-relative tw-z-above" >
{ input = ( < input
2021-05-19 16:59:26 -04:00
class = "ffz-range ffz-range--overlay"
2021-04-24 14:37:01 -04:00
type = "range"
min = "0"
max = "100"
step = "1"
data - a - target = "player-gain-slider"
value = "100"
/ > ) }
2021-05-19 16:59:26 -04:00
< div class = "tw-absolute tw-border-radius-large tw-bottom-0 tw-flex tw-flex-column tw-full-width tw-justify-content-center ffz-range__fill ffz-range__fill--overlay tw-top-0 tw-z-below" >
< div class = "tw-border-radius-large ffz-range__fill-container" >
2021-04-24 14:37:01 -04:00
{ fill = ( < div
2021-05-19 16:59:26 -04:00
class = "tw-border-radius-large ffz-range__fill-value ffz--gain-value"
data - test - selector = "ffz-range__fill-value-selector"
2021-04-24 14:37:01 -04:00
/ > ) }
< / div >
< / div >
< / div >
< / div >
2021-05-17 17:02:23 -04:00
{ tipcont = ( < div class = "ffz-il-tooltip ffz-il-tooltip--align-center ffz-il-tooltip--up" role = "tooltip" >
2021-04-24 14:37:01 -04:00
< div >
{ tip = ( < div class = "ffz--p-tip" / > ) }
{ extra = ( < div class = "tw-regular ffz--p-value" / > ) }
< / div >
2021-04-28 00:51:24 -04:00
< / div > ) }
2021-04-24 14:37:01 -04:00
< / div > ) ;
/ * i n p u t . a d d E v e n t L i s t e n e r ( ' c o n t e x t m e n u ' , e = > {
video . _ffz _gain _value = null ;
this . updateGain ( inst ) ;
e . preventDefault ( ) ;
} ) ; * /
input . addEventListener ( 'input' , on _change ) ;
container . appendChild ( cont ) ;
} else if ( visible _only )
return ;
else {
input = cont . querySelector ( 'input' ) ;
fill = cont . querySelector ( '.ffz--gain-value' ) ;
2021-05-17 17:02:23 -04:00
tipcont = cont . querySelector ( '.ffz-il-tooltip' ) ;
tip = cont . querySelector ( '.ffz-il-tooltip .ffz--p-tip' ) ;
extra = cont . querySelector ( '.ffz-il-tooltip .ffz--p-value' ) ;
2021-04-24 14:37:01 -04:00
}
let value = video . _ffz _gain _value ;
if ( value == null )
value = this . settings . get ( 'player.gain.default' ) ;
input . min = min * 100 ;
input . max = max * 100 ;
input . value = value * 100 ;
const range = max - min ,
width = ( value - min ) / range ;
fill . style . width = ` ${ width * 100 } % ` ;
tip . textContent = this . i18n . t ( 'player.gain.label' , 'Gain Control' ) ;
extra . textContent = ` ${ Math . round ( value * 100 ) } % ` ;
}
2021-02-26 15:35:26 -05:00
addCompressorButton ( inst , visible _only , tries = 0 ) {
const outer = inst . props . containerRef || this . fine . getChildNode ( inst ) ,
video = inst . props . mediaPlayerInstance ? . mediaSinkManager ? . video || inst . props . mediaPlayerInstance ? . core ? . mediaSinkManager ? . video ,
2022-08-02 16:59:50 -04:00
container = outer && outer . querySelector ( LEFT _CONTROLS ) ,
2021-02-26 15:35:26 -05:00
has _comp = HAS _COMPRESSOR && video != null && this . settings . get ( 'player.compressor.enable' ) ;
if ( ! container ) {
if ( ! has _comp )
return ;
if ( tries < 5 )
return setTimeout ( this . addCompressorButton . bind ( this , inst , visible _only , ( tries || 0 ) + 1 ) , 250 ) ;
return ;
}
2022-09-01 15:36:47 -04:00
let icon , tip , extra , btn , cont = container . querySelector ( '.ffz--player-comp' ) ;
2021-05-29 18:14:29 -04:00
if ( ! has _comp || this . areControlsDisabled ( inst ) ) {
2021-02-26 15:35:26 -05:00
if ( cont )
cont . remove ( ) ;
return ;
}
if ( ! cont ) {
2021-05-17 17:02:23 -04:00
cont = ( < div class = "ffz--player-comp tw-inline-flex tw-relative ffz-il-tooltip__container" >
2021-02-26 15:35:26 -05:00
{ btn = ( < button
class = "tw-align-items-center tw-align-middle tw-border-bottom-left-radius-medium tw-border-bottom-right-radius-medium tw-border-top-left-radius-medium tw-border-top-right-radius-medium tw-button-icon tw-button-icon--overlay ffz-core-button ffz-core-button--border ffz-core-button--overlay tw-inline-flex tw-interactive tw-justify-content-center tw-overflow-hidden tw-relative"
type = "button"
data - a - target = "ffz-player-comp-button"
onClick = { this . compressPlayer . bind ( this , inst ) } // eslint-disable-line react/jsx-no-bind
>
< div class = "tw-align-items-center tw-flex tw-flex-grow-0" >
< div class = "tw-button-icon__icon" >
{ icon = ( < figure class = "ffz-player-icon" / > ) }
< / div >
< / div >
< / button > ) }
2021-05-17 17:02:23 -04:00
< div class = "ffz-il-tooltip ffz-il-tooltip--align-left ffz-il-tooltip--up" role = "tooltip" >
2021-02-26 15:35:26 -05:00
< div >
{ tip = ( < div class = "ffz--p-tip" / > ) }
{ extra = ( < div class = "ffz--p-extra tw-pd-t-05 ffz--tooltip-explain" / > ) }
< / div >
< / div >
< / div > ) ;
container . appendChild ( cont ) ;
} else if ( visible _only )
return ;
else {
icon = cont . querySelector ( 'figure' ) ;
btn = cont . querySelector ( 'button' ) ;
2021-05-17 17:02:23 -04:00
tip = cont . querySelector ( '.ffz-il-tooltip .ffz--p-tip' ) ;
extra = cont . querySelector ( '.ffz-il-tooltip .ffz--p-extra' ) ;
2021-02-26 15:35:26 -05:00
}
const comp _active = video . _ffz _compressed ,
2021-04-24 14:37:01 -04:00
can _apply = this . canCompress ( inst ) ;
let label = can _apply ?
comp _active ?
this . i18n . t ( 'player.comp_button.off' , 'Disable Audio Compressor' ) :
this . i18n . t ( 'player.comp_button.on' , 'Audio Compressor' )
: this . i18n . t ( 'player.comp_button.disabled' , 'Audio Compressor cannot be enabled when viewing Clips.' ) ;
2021-02-26 15:35:26 -05:00
extra . textContent = this . i18n . t ( 'player.comp_button.help' , 'See the FFZ Control Center for details. If audio breaks, please reset the player.' ) ;
2021-04-24 14:37:01 -04:00
if ( can _apply && this . _shortcut _bound )
label = ` ${ label } ( ${ this . _shortcut _bound } ) ` ;
2021-02-26 15:35:26 -05:00
icon . classList . toggle ( 'ffz-i-comp-on' , comp _active ) ;
icon . classList . toggle ( 'ffz-i-comp-off' , ! comp _active ) ;
btn . disabled = ! can _apply ;
btn . setAttribute ( 'aria-label' , label ) ;
tip . textContent = label ;
}
compressPlayer ( inst , e ) {
2021-04-24 14:37:01 -04:00
const player = inst . props . mediaPlayerInstance ,
core = player . core || player ,
video = core ? . mediaSinkManager ? . video ;
2021-02-26 15:35:26 -05:00
if ( ! video || ! HAS _COMPRESSOR )
return ;
2021-04-24 14:37:01 -04:00
// Backup the setVolume method.
if ( ! core . _ffz _setVolume ) {
core . _ffz _setVolume = core . setVolume ;
core . _ffz _fakeVolume = ( ) => { } ;
}
2021-04-14 16:53:15 -04:00
video . _ffz _maybe _compress = false ;
2021-02-26 15:35:26 -05:00
const compressed = video . _ffz _compressed || false ;
2021-04-14 16:53:15 -04:00
let wanted = video . _ffz _toggled ? video . _ffz _state : this . settings . get ( 'player.compressor.default' ) ;
2021-02-26 15:35:26 -05:00
if ( e != null ) {
e . preventDefault ( ) ;
video . _ffz _toggled = true ;
wanted = ! video . _ffz _compressed ;
2021-04-14 16:53:15 -04:00
video . _ffz _state = wanted ;
2021-02-26 15:35:26 -05:00
}
if ( ! video . _ffz _compressor ) {
if ( ! wanted )
return ;
this . createCompressor ( inst , video ) ;
} else if ( ! video . _ffz _comp _reset && ! this . canCompress ( inst ) ) {
video . _ffz _comp _reset = true ;
this . resetPlayer ( inst ) ;
return ;
}
2021-04-24 14:37:01 -04:00
let gain = video . _ffz _gain ;
const want _gain = HAS _GAIN && this . settings . get ( 'player.gain.enable' ) ,
has _gain = gain != null ;
if ( ( ( wanted == compressed ) || ( e == null && video . _ffz _toggled ) ) && has _gain == want _gain )
2021-02-26 15:35:26 -05:00
return ;
const ctx = video . _ffz _context ,
comp = video . _ffz _compressor ,
src = video . _ffz _source ;
if ( ! ctx || ! comp || ! src )
return ;
2021-04-24 14:37:01 -04:00
if ( want _gain && ! gain ) {
let value = video . _ffz _gain _value ;
if ( value == null )
value = this . settings . get ( 'player.gain.default' ) ;
2022-04-25 14:19:34 -04:00
try {
2022-05-05 13:41:49 -04:00
if ( this . settings . get ( 'player.compressor.force-legacy' ) )
throw new Error ( ) ;
2022-04-25 14:19:34 -04:00
gain = video . _ffz _gain = new GainNode ( ctx , {
gain : value
} ) ;
} catch ( err ) {
this . log . info ( 'Unable to use new GainNode. Falling back to old method.' ) ;
gain = video . _ffz _gain = ctx . createGain ( ) ;
gain . gain . value = value ;
}
2021-04-24 14:37:01 -04:00
comp . connect ( gain ) ;
if ( compressed ) {
comp . disconnect ( ctx . destination ) ;
gain . connect ( ctx . destination ) ;
}
} else if ( ! want _gain && gain ) {
comp . disconnect ( gain ) ;
if ( compressed ) {
gain . disconnect ( ctx . destination ) ;
comp . connect ( ctx . destination ) ;
}
gain = video . _ffz _gain = null ;
}
if ( wanted != compressed ) {
if ( wanted ) {
src . disconnect ( ctx . destination ) ;
src . connect ( comp ) ;
if ( gain ) {
gain . connect ( ctx . destination ) ;
if ( this . settings . get ( 'player.gain.no-volume' ) ) {
video . _ffz _pregain _volume = core . getVolume ( ) ;
core . _ffz _setVolume ( 1 ) ;
core . setVolume = core . _ffz _fakeVolume ;
}
} else
comp . connect ( ctx . destination ) ;
} else {
src . disconnect ( comp ) ;
if ( gain ) {
gain . disconnect ( ctx . destination ) ;
if ( video . _ffz _pregain _volume != null ) {
core . _ffz _setVolume ( video . _ffz _pregain _volume ) ;
core . setVolume = core . _ffz _setVolume ;
video . _ffz _pregain _volume = null ;
}
} else
comp . disconnect ( ctx . destination ) ;
src . connect ( ctx . destination ) ;
}
2021-02-26 15:35:26 -05:00
}
2021-04-24 14:37:01 -04:00
if ( inst . props . containerRef )
inst . props . containerRef . dataset . compressed = wanted ;
2021-02-26 15:35:26 -05:00
video . _ffz _compressed = wanted ;
this . addCompressorButton ( inst ) ;
2021-04-24 14:37:01 -04:00
this . addGainSlider ( inst ) ;
}
updateGainVolume ( inst ) {
const player = inst . props . mediaPlayerInstance ,
core = player . core || player ,
video = core ? . mediaSinkManager ? . video ;
if ( ! video || ! video . _ffz _compressed )
return ;
const setting = this . settings . get ( 'player.gain.no-volume' ) ;
if ( setting && video . _ffz _pregain _volume == null ) {
video . _ffz _pregain _volume = core . getVolume ( ) ;
core . _ffz _setVolume ( 1 ) ;
core . setVolume = core . _ffz _fakeVolume ;
} else if ( ! setting && video . _ffz _pregain _volume != null ) {
core . _ffz _setVolume ( video . _ffz _pregain _volume ) ;
core . setVolume = core . _ffz _setVolume ;
video . _ffz _pregain _volume = null ;
}
2021-02-26 15:35:26 -05:00
}
canCompress ( inst ) { // eslint-disable-line class-methods-use-this
if ( ! HAS _COMPRESSOR )
return false ;
const player = inst . props ? . mediaPlayerInstance ;
if ( player == null )
return false ;
const video = player . mediaSinkManager ? . video || player . core ? . mediaSinkManager ? . video ;
if ( ! video )
return false ;
2022-12-09 18:23:30 -08:00
if ( ! video . src && ! video . srcObject )
return false ;
2021-02-26 15:35:26 -05:00
if ( video . src ) {
const url = new URL ( video . src ) ;
if ( url . protocol !== 'blob:' )
return false ;
2022-12-09 18:23:30 -08:00
}
2021-02-26 15:35:26 -05:00
return true ;
}
2022-04-25 15:01:40 -04:00
createCompressor ( inst , video , _cmp ) {
2021-02-26 15:35:26 -05:00
if ( ! this . canCompress ( inst ) )
return ;
let comp = video . _ffz _compressor ;
if ( ! comp ) {
2022-04-25 15:01:40 -04:00
const ctx = _cmp || new AudioContext ( ) ;
2021-02-26 15:35:26 -05:00
if ( ! IS _FIREFOX && ctx . state === 'suspended' ) {
2022-04-25 15:01:40 -04:00
let timer ;
const evt = ( ) => {
clearTimeout ( timer ) ;
ctx . removeEventListener ( 'statechange' , evt ) ;
if ( ctx . state === 'suspended' ) {
this . log . info ( 'Aborting due to browser auto-play policy.' ) ;
return ;
}
this . createCompressor ( inst , video , comp ) ;
}
this . log . info ( 'Attempting to resume suspended AudioContext.' ) ;
timer = setTimeout ( evt , 100 ) ;
try {
ctx . addEventListener ( 'statechange' , evt ) ;
ctx . resume ( ) ;
} catch ( err ) { }
2021-02-26 15:35:26 -05:00
return ;
}
video . _ffz _context = ctx ;
2022-04-25 14:19:34 -04:00
let src ;
try {
2022-05-05 13:41:49 -04:00
if ( this . settings . get ( 'player.compressor.force-legacy' ) )
throw new Error ( ) ;
2022-04-25 14:19:34 -04:00
src = video . _ffz _source = new MediaElementAudioSourceNode ( ctx , {
mediaElement : video
} ) ;
} catch ( err ) {
this . log . info ( 'Unable to use new MediaElementAudioSourceNode. Falling back to old method.' ) ;
src = video . _ffz _source = ctx . createMediaElementSource ( video ) ;
}
2021-02-26 15:35:26 -05:00
src . connect ( ctx . destination ) ;
2022-04-25 13:47:10 -04:00
try {
2022-05-05 13:41:49 -04:00
if ( this . settings . get ( 'player.compressor.force-legacy' ) )
throw new Error ( ) ;
2022-04-25 13:47:10 -04:00
comp = video . _ffz _compressor = new DynamicsCompressorNode ( ctx ) ;
} catch ( err ) {
2022-04-25 14:19:34 -04:00
this . log . info ( 'Unable to use new DynamicsCompressorNode. Falling back to old method.' ) ;
2022-04-25 13:47:10 -04:00
comp = video . _ffz _compressor = ctx . createDynamicsCompressor ( ) ;
}
2021-04-24 14:37:01 -04:00
if ( this . settings . get ( 'player.gain.enable' ) ) {
2022-04-25 13:47:10 -04:00
let gain ;
2021-04-24 14:37:01 -04:00
let value = video . _ffz _gain _value ;
if ( value == null )
value = this . settings . get ( 'player.gain.default' ) ;
2022-04-25 13:47:10 -04:00
try {
2022-05-05 13:41:49 -04:00
if ( this . settings . get ( 'player.compressor.force-legacy' ) )
throw new Error ( ) ;
2022-04-25 13:47:10 -04:00
gain = video . _ffz _gain = new GainNode ( ctx , {
gain : value
} ) ;
} catch ( err ) {
2022-04-25 14:19:34 -04:00
this . log . info ( 'Unable to use new GainNode. Falling back to old method.' ) ;
2022-04-25 13:47:10 -04:00
gain = video . _ffz _gain = ctx . createGain ( ) ;
gain . gain . value = value ;
}
2021-04-24 14:37:01 -04:00
comp . connect ( gain ) ;
}
2021-02-26 15:35:26 -05:00
video . _ffz _compressed = false ;
}
this . updateCompressor ( null , comp ) ;
}
2021-04-24 14:37:01 -04:00
updateGains ( ) {
for ( const inst of this . Player . instances )
this . updateGain ( inst ) ;
}
updateGain ( inst , gain , video , update _gui = true ) {
if ( ! video )
video = inst . props . mediaPlayerInstance ? . mediaSinkManager ? . video ||
inst . props . mediaPlayerInstance ? . core ? . mediaSinkManager ? . video ;
if ( gain == null )
gain = video ? . _ffz _gain ;
if ( ! video || ! gain )
return ;
let value = video . _ffz _gain _value ;
if ( value == null )
value = this . settings . get ( 'player.gain.default' ) ;
gain . gain . value = value ;
if ( update _gui )
this . addGainSlider ( inst ) ;
}
2021-02-26 15:35:26 -05:00
updateCompressors ( ) {
for ( const inst of this . Player . instances )
this . updateCompressor ( inst ) ;
}
updateCompressor ( inst , comp ) {
if ( comp == null ) {
const video = inst . props . mediaPlayerInstance ? . mediaSinkManager ? . video ||
inst . props . mediaPlayerInstance ? . core ? . mediaSinkManager ? . video ;
comp = video ? . _ffz _compressor ;
}
if ( ! comp )
return ;
comp . threshold . value = this . settings . get ( 'player.compressor.threshold' ) ;
comp . knee . value = this . settings . get ( 'player.compressor.knee' ) ;
comp . ratio . value = this . settings . get ( 'player.compressor.ratio' ) ;
comp . attack . value = this . settings . get ( 'player.compressor.attack' ) ;
comp . release . value = this . settings . get ( 'player.compressor.release' ) ;
}
updatePlaybackRates ( ) {
for ( const inst of this . Player . instances )
this . updatePlaybackRate ( inst ) ;
}
updatePlaybackRate ( inst ) {
const video = inst . props . mediaPlayerInstance ? . mediaSinkManager ? . video ||
inst . props . mediaPlayerInstance ? . core ? . mediaSinkManager ? . video ;
if ( ! video . setFFZPlaybackRate )
this . installPlaybackRate ( video ) ;
video . setFFZPlaybackRate ( video . playbackRate ) ;
}
installPlaybackRate ( video ) {
if ( video . setFFZPlaybackRate )
return ;
let pbrate = video . playbackRate ;
const t = this ,
installProperty = ( ) => {
if ( t . settings . get ( 'player.allow-catchup' ) )
return ;
Object . defineProperty ( video , 'playbackRate' , {
configurable : true ,
get ( ) {
return pbrate ;
} ,
set ( val ) {
if ( val === 1 || val < 1 || val >= 1.1 )
video . setFFZPlaybackRate ( val ) ;
}
} ) ;
}
video . setFFZPlaybackRate = rate => {
delete video . playbackRate ;
pbrate = rate ;
video . playbackRate = rate ;
installProperty ( ) ;
} ;
}
addPiPButton ( inst , tries = 0 ) {
const outer = inst . props . containerRef || this . fine . getChildNode ( inst ) ,
video = inst . props . mediaPlayerInstance ? . mediaSinkManager ? . video || inst . props . mediaPlayerInstance ? . core ? . mediaSinkManager ? . video ,
is _fs = video && document . fullscreenElement && document . fullscreenElement . contains ( video ) ,
2022-08-02 16:59:50 -04:00
container = outer && outer . querySelector ( RIGHT _CONTROLS ) ,
2021-02-26 15:35:26 -05:00
has _pip = document . pictureInPictureEnabled && this . settings . get ( 'player.button.pip' ) ;
if ( ! container ) {
if ( ! has _pip )
return ;
if ( tries < 5 )
return setTimeout ( this . addPiPButton . bind ( this , inst , ( tries || 0 ) + 1 ) , 250 ) ;
return ; // this.log.warn('Unable to find container element for PiP button.');
}
let icon , tip , btn , cont = container . querySelector ( '.ffz--player-pip' ) ;
if ( ! has _pip ) {
if ( cont )
cont . remove ( ) ;
return ;
}
if ( ! cont ) {
2021-05-17 17:02:23 -04:00
cont = ( < div class = "ffz--player-pip tw-inline-flex tw-relative ffz-il-tooltip__container" >
2021-02-26 15:35:26 -05:00
{ btn = ( < button
class = "tw-align-items-center tw-align-middle tw-border-bottom-left-radius-medium tw-border-bottom-right-radius-medium tw-border-top-left-radius-medium tw-border-top-right-radius-medium tw-button-icon tw-button-icon--overlay ffz-core-button ffz-core-button--border ffz-core-button--overlay tw-inline-flex tw-interactive tw-justify-content-center tw-overflow-hidden tw-relative"
type = "button"
data - a - target = "ffz-player-pip-button"
onClick = { this . pipPlayer . bind ( this , inst ) } // eslint-disable-line react/jsx-no-bind
>
< div class = "tw-align-items-center tw-flex tw-flex-grow-0" >
< div class = "tw-button-icon__icon" >
{ icon = ( < figure class = "ffz-player-icon" / > ) }
< / div >
< / div >
< / button > ) }
2021-05-17 17:02:23 -04:00
{ tip = ( < div class = "ffz-il-tooltip ffz-il-tooltip--align-right ffz-il-tooltip--up" role = "tooltip" / > ) }
2021-02-26 15:35:26 -05:00
< / div > ) ;
let thing = container . querySelector ( 'button[data-a-target="player-theatre-mode-button"]' ) ;
if ( ! thing )
thing = container . querySelector ( 'button[data-a-target="player-fullscreen-button"]' ) ;
if ( thing ) {
container . insertBefore ( cont , thing . parentElement ) ;
} else
container . appendChild ( cont ) ;
} else {
icon = cont . querySelector ( 'figure' ) ;
btn = cont . querySelector ( 'button' ) ;
2021-05-17 17:02:23 -04:00
tip = cont . querySelector ( '.ffz-il-tooltip' ) ;
2021-02-26 15:35:26 -05:00
}
const pip _active = ! ! document . pictureInPictureElement ,
pip _swap = false , //pip_active && document.pictureInPictureElement !== video,
label = is _fs ?
this . i18n . t ( 'player.pip_button.fs' , 'Cannot use Picture-in-Picture when Fullscreen' )
: pip _swap ?
this . i18n . t ( 'player.pip_button.swap' , 'Switch Picture-in-Picture' )
: pip _active ?
this . i18n . t ( 'player.pip_button.off' , 'Exit Picture-in-Picture' )
: this . i18n . t ( 'player.pip_button' , 'Picture-in-Picture' ) ;
icon . classList . toggle ( 'ffz-i-t-pip-inactive' , ! pip _active || pip _swap ) ;
icon . classList . toggle ( 'ffz-i-t-pip-active' , pip _active && ! pip _swap ) ;
btn . setAttribute ( 'aria-label' , label ) ;
tip . textContent = label ;
}
pipPlayer ( inst , e ) {
const video = inst . props . mediaPlayerInstance ? . mediaSinkManager ? . video ||
inst . props . mediaPlayerInstance ? . core ? . mediaSinkManager ? . video ;
if ( ! video || ! document . pictureInPictureEnabled )
return ;
if ( e )
e . preventDefault ( ) ;
if ( document . fullscreenElement && document . fullscreenElement . contains ( video ) )
return ;
if ( ! video . _ffz _pip _enter ) {
video . addEventListener ( 'enterpictureinpicture' , video . _ffz _pip _enter = ( ) => {
this . addPiPButton ( inst ) ;
} ) ;
video . addEventListener ( 'leavepictureinpicture' , video . _ffz _pip _exit = ( ) => {
this . addPiPButton ( inst ) ;
} ) ;
}
//const is_this = document.pictureInPictureElement === video;
if ( document . pictureInPictureElement )
document . exitPictureInPicture ( ) ;
else
//if ( ! is_this )
video . requestPictureInPicture ( ) ;
}
addResetButton ( inst , tries = 0 ) {
const outer = inst . props . containerRef || this . fine . getChildNode ( inst ) ,
2022-08-02 16:59:50 -04:00
container = outer && outer . querySelector ( RIGHT _CONTROLS ) ,
2021-02-26 15:35:26 -05:00
has _reset = this . settings . get ( 'player.button.reset' ) ;
if ( ! container ) {
if ( ! has _reset )
return ;
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 , btn , cont = container . querySelector ( '.ffz--player-reset' ) ;
if ( ! has _reset ) {
if ( cont )
cont . remove ( ) ;
return ;
}
if ( ! cont ) {
2021-05-17 17:02:23 -04:00
cont = ( < div class = "ffz--player-reset tw-inline-flex tw-relative ffz-il-tooltip__container" >
2021-02-26 15:35:26 -05:00
{ btn = ( < button
class = "tw-align-items-center tw-align-middle tw-border-bottom-left-radius-medium tw-border-bottom-right-radius-medium tw-border-top-left-radius-medium tw-border-top-right-radius-medium tw-button-icon tw-button-icon--overlay ffz-core-button ffz-core-button--border ffz-core-button--overlay tw-inline-flex tw-interactive tw-justify-content-center tw-overflow-hidden tw-relative"
type = "button"
data - a - target = "ffz-player-reset-button"
onClick = { rotateButton }
onDblClick = { this . resetPlayer . bind ( this , inst ) } // eslint-disable-line react/jsx-no-bind
>
< div class = "tw-align-items-center tw-flex tw-flex-grow-0" >
< div class = "tw-button-icon__icon" >
< figure class = "ffz-player-icon ffz-i-t-reset" / >
< / div >
< / div >
< / button > ) }
2021-05-17 17:02:23 -04:00
{ tip = ( < div class = "ffz-il-tooltip ffz-il-tooltip--align-right ffz-il-tooltip--up" role = "tooltip" / > ) }
2021-02-26 15:35:26 -05:00
< / div > ) ;
const thing = container . querySelector ( '.ffz--player-pip button' ) || container . querySelector ( 'button[data-a-target="player-theatre-mode-button"]' ) || container . querySelector ( 'button[data-a-target="player-fullscreen-button"]' ) ;
if ( thing ) {
container . insertBefore ( cont , thing . parentElement ) ;
} else
container . appendChild ( cont ) ;
} else {
btn = cont . querySelector ( 'button' ) ;
2021-05-17 17:02:23 -04:00
tip = cont . querySelector ( '.ffz-il-tooltip' ) ;
2021-02-26 15:35:26 -05:00
}
btn . setAttribute ( 'aria-label' ,
tip . textContent = this . i18n . t (
'player.reset_button' ,
'Reset Player (Double-Click)'
) ) ;
}
addErrorResetButton ( inst , tries = 0 ) {
const outer = inst . props . containerRef || this . fine . getChildNode ( inst ) ,
container = outer && outer . querySelector ( '.content-overlay-gate' ) ,
has _reset = this . settings . get ( 'player.button.reset' ) ;
if ( ! container ) {
if ( ! has _reset )
return ;
if ( tries < 2 )
this . parent . awaitElement (
'.autoplay-vod__content-container button' ,
this . props . containerRef || t . fine . getChildNode ( this ) ,
1000
) . then ( ( ) => {
this . addErrorResetButton ( inst , ( tries || 0 ) + 1 ) ;
} ) . catch ( ( ) => {
this . log . warn ( 'Unable to find container element for Error Reset button.' ) ;
} ) ;
return ;
}
let tip , btn , cont = container . querySelector ( '.ffz--player-reset' ) ;
if ( ! has _reset ) {
if ( cont )
cont . remove ( ) ;
return ;
}
if ( ! cont ) {
2021-05-17 17:02:23 -04:00
cont = ( < div class = "ffz--player-reset tw-absolute tw-bottom-0 tw-right-0 ffz-il-tooltip__container tw-mg-1" >
2021-02-26 15:35:26 -05:00
{ btn = ( < button
class = "tw-align-items-center tw-align-middle tw-border-bottom-left-radius-medium tw-border-bottom-right-radius-medium tw-border-top-left-radius-medium tw-border-top-right-radius-medium tw-button-icon tw-button-icon--overlay ffz-core-button ffz-core-button--border ffz-core-button--overlay tw-inline-flex tw-interactive tw-justify-content-center tw-overflow-hidden tw-relative"
type = "button"
data - a - target = "ffz-player-reset-button"
onClick = { rotateButton }
onDblClick = { this . resetPlayer . bind ( this , inst ) } // eslint-disable-line react/jsx-no-bind
>
< div class = "tw-align-items-center tw-flex tw-flex-grow-0" >
< div class = "tw-button-icon__icon" >
< figure class = "ffz-player-icon ffz-i-t-reset" / >
< / div >
< / div >
< / button > ) }
2021-05-17 17:02:23 -04:00
{ tip = ( < div class = "ffz-il-tooltip ffz-il-tooltip--align-right ffz-il-tooltip--up" role = "tooltip" / > ) }
2021-02-26 15:35:26 -05:00
< / div > ) ;
container . appendChild ( cont ) ;
} else {
btn = cont . querySelector ( 'button' ) ;
2021-05-17 17:02:23 -04:00
tip = cont . querySelector ( '.ffz-il-tooltip' ) ;
2021-02-26 15:35:26 -05:00
}
btn . setAttribute ( 'aria-label' ,
tip . textContent = this . i18n . t (
'player.reset_button' ,
'Double-Click to Reset Player'
) ) ;
}
resetAllPlayers ( ) {
for ( const inst of this . Player . instances )
this . resetPlayer ( inst ) ;
}
resetPlayer ( inst , e ) {
const player = inst ? ( ( inst . mediaSinkManager || inst . core ? . mediaSinkManager ) ? inst : inst ? . props ? . mediaPlayerInstance ) : null ;
if ( e ) {
e . preventDefault ( ) ;
const target = e . currentTarget ,
icon = target && target . querySelector ( 'figure' ) ;
if ( icon ) {
if ( icon . classList . contains ( 'loading' ) )
return ;
icon . classList . toggle ( 'ffz-i-t-reset' , true ) ;
icon . classList . toggle ( 'ffz-i-t-reset-clicked' , false ) ;
icon . classList . toggle ( 'loading' , true ) ;
icon . _ffz _unspin = setTimeout ( ( ) => {
icon . _ffz _unspin = null ;
icon . classList . toggle ( 'loading' , false ) ;
} , 10000 ) ;
}
}
// Are we dealing with a VOD?
const duration = player . getDuration ? . ( ) ? ? Infinity ;
let position = - 1 ;
2021-04-24 14:37:01 -04:00
const core = player . core || player ;
if ( core . _ffz _setVolume )
core . setVolume = core . _ffz _setVolume ;
2021-02-26 15:35:26 -05:00
if ( isFinite ( duration ) && ! isNaN ( duration ) && duration > 0 )
position = player . getPosition ( ) ;
const video = player . mediaSinkManager ? . video || player . core ? . mediaSinkManager ? . video ;
if ( video ? . _ffz _compressor && player . attachHTMLVideoElement ) {
const new _vid = createElement ( 'video' ) ,
2021-04-24 14:37:01 -04:00
vol = video ? . _ffz _pregain _volume ? ? video ? . volume ? ? player . getVolume ( ) ,
2021-02-26 15:35:26 -05:00
muted = player . isMuted ( ) ;
2021-04-14 16:53:15 -04:00
2021-04-24 14:37:01 -04:00
new _vid . _ffz _gain _value = video . _ffz _gain _value ;
2021-04-14 16:53:15 -04:00
new _vid . _ffz _state = video . _ffz _state ;
new _vid . _ffz _toggled = video . _ffz _toggled ;
new _vid . _ffz _maybe _compress = true ;
2021-02-26 15:35:26 -05:00
new _vid . volume = muted ? 0 : vol ;
new _vid . playsInline = true ;
2021-04-24 14:37:01 -04:00
2021-02-26 15:35:26 -05:00
this . installPlaybackRate ( new _vid ) ;
video . replaceWith ( new _vid ) ;
player . attachHTMLVideoElement ( new _vid ) ;
setTimeout ( ( ) => {
player . setVolume ( vol ) ;
player . setMuted ( muted ) ;
//localStorage.volume = vol;
//localStorage.setItem('video-muted', JSON.stringify({default: muted}));
} , 0 ) ;
}
this . PlayerSource . check ( ) ;
for ( const inst of this . PlayerSource . instances ) {
if ( ! player || player === inst . props ? . mediaPlayerInstance )
inst . setSrc ( { isNewMediaPlayerInstance : false } ) ;
}
if ( position > 0 )
setTimeout ( ( ) => player . seekTo ( position ) , 250 ) ;
}
addMetadata ( inst ) {
if ( ! this . metadata )
return ;
if ( ! inst . _ffz _md _update )
inst . _ffz _md _update = debounce ( ( ) => requestAnimationFrame ( ( ) => this . _updateMetadata ( inst ) ) , 1000 , 2 ) ;
inst . _ffz _md _update ( ) ;
}
wantsMetadata ( ) { // eslint-disable-line class-methods-use-this
return false ;
}
_updateMetadata ( inst ) {
if ( inst . _ffz _cont && ! document . contains ( inst . _ffz _cont ) )
inst . _ffz _cont = null ;
const wanted = this . wantsMetadata ( inst ) ;
if ( ! inst . _ffz _cont ) {
if ( ! wanted )
return ;
const outer = inst . props . containerRef || this . fine . getChildNode ( inst ) ,
2022-08-02 16:59:50 -04:00
container = outer && outer . querySelector ( RIGHT _CONTROLS ) ;
2021-02-26 15:35:26 -05:00
if ( ! container )
return ;
inst . _ffz _cont = ( < div class = "ffz--player-meta-tray" / > ) ;
container . insertBefore ( inst . _ffz _cont , container . firstElementChild ) ;
}
if ( ! wanted ) {
inst . _ffz _cont . remove ( ) ;
inst . _ffz _cont = null ;
return ;
}
this . updateMetadata ( inst ) ;
}
updateMetadata ( inst , keys ) {
const cont = inst . _ffz _cont ;
if ( ! cont || ! document . contains ( cont ) )
return ;
if ( ! keys )
keys = this . metadata . keys ;
else if ( ! Array . isArray ( keys ) )
keys = [ keys ] ;
const source = this . parent . data ,
user = source ? . props ? . data ? . user ;
const timers = inst . _ffz _meta _timers = inst . _ffz _meta _timers || { } ,
refresh _fn = key => this . updateMetadata ( inst , key ) ,
data = {
channel : {
id : user ? . id ,
login : source ? . props ? . channelLogin ,
display _name : user ? . displayName ,
live : user ? . stream ? . id != null ,
live _since : user ? . stream ? . createdAt
} ,
inst ,
source ,
getViewerCount : ( ) => 0 ,
getUserSelfImmediate : ( ) => null ,
getUserSelf : ( ) => null ,
getBroadcastID : ( ) => user ? . id ? this . getBroadcastID ( inst , user . id ) : null
} ;
for ( const key of keys )
this . metadata . renderPlayer ( key , data , cont , timers , refresh _fn ) ;
}
getBroadcastID ( inst , channel _id ) {
if ( ! this . twitch _data )
return Promise . resolve ( null ) ;
const cache = inst . _ffz _bcast _cache = inst . _ffz _bcast _cache || { } ;
if ( channel _id === cache . channel _id ) {
if ( Date . now ( ) - cache . saved < 60000 )
return Promise . resolve ( cache . broadcast _id ) ;
}
return new Promise ( async ( s , f ) => {
if ( cache . updating ) {
cache . updating . push ( [ s , f ] ) ;
return ;
}
cache . channel _id = channel _id ;
cache . updating = [ [ s , f ] ] ;
let id , err ;
try {
id = await this . twitch _data . getBroadcastID ( channel _id ) ;
} catch ( error ) {
id = null ;
err = error ;
}
const waiters = cache . updating ;
cache . updating = null ;
if ( cache . channel _id !== channel _id ) {
err = new Error ( 'Outdated' ) ;
cache . channel _id = null ;
cache . broadcast _id = null ;
cache . saved = 0 ;
for ( const pair of waiters )
pair [ 1 ] ( err ) ;
return ;
}
cache . broadcast _id = id ;
cache . saved = Date . now ( ) ;
for ( const pair of waiters )
err ? pair [ 1 ] ( err ) : pair [ 0 ] ( id ) ;
} ) ;
}
get playerUI ( ) {
const container = this . fine . searchTree ( this . Player . first , n => n . props && n . props . uiContext , 150 ) ;
return container ? . props ? . uiContext ;
}
get current ( ) {
for ( const inst of this . Player . instances )
if ( inst ? . props ? . mediaPlayerInstance )
return inst . props . mediaPlayerInstance ;
return null ;
}
2022-12-09 18:37:25 -08:00
}