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' ;
import { debounce } from 'utilities/object' ;
import { IS _FIREFOX } from 'src/utilities/constants' ;
const STYLE _VALIDATOR = createElement ( 'span' ) ;
const HAS _COMPRESSOR = window . AudioContext && window . DynamicsCompressorNode != null ;
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' ) ;
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'
}
} ) ;
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 ) ;
}
} ) ;
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' ,
process ( val ) {
val = parseInt ( val , 10 ) ;
if ( isNaN ( val ) || ! isFinite ( val ) || val > 0 || val < - 100 )
return - 50 ;
return val ;
}
} ,
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' ,
process ( val ) {
val = parseInt ( val , 10 ) ;
if ( isNaN ( val ) || ! isFinite ( val ) || val < 0 || val > 40 )
return 40 ;
return val ;
}
} ,
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' ,
process ( val ) {
val = parseInt ( val , 10 ) ;
if ( isNaN ( val ) || ! isFinite ( val ) || val < 1 || val > 20 )
return 12 ;
return val ;
}
} ,
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' ,
process ( val ) {
val = parseFloat ( val ) ;
if ( isNaN ( val ) || ! isFinite ( val ) || val < 0 || val > 1 )
return 0 ;
return val ;
}
} ,
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' ,
process ( val ) {
val = parseFloat ( val ) ;
if ( isNaN ( val ) || ! isFinite ( val ) || val < 0 || val > 1 )
return 0.25 ;
return val ;
}
} ,
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' ,
data : [
{ value : false , title : 'Disabled' } ,
{ value : true , title : 'Enabled' } ,
{ value : 2 , title : 'Enabled with Right-Click' }
]
}
} ) ;
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.' ,
component : 'setting-text-box'
} ,
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' ,
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 )
} ) ;
}
async onEnable ( ) {
await this . settings . awaitProvider ( ) ;
await this . settings . provider . awaitReady ( ) ;
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' ) ) ;
this . installVisibilityHook ( ) ;
this . updateHideExtensions ( ) ;
this . updateCaptionsCSS ( ) ;
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 ) ;
}
} ) ;
}
modifyPlayerClass ( cls ) {
const t = this ,
old _attach = cls . prototype . maybeAttachDomEventListeners ;
cls . prototype . ffzInstall = function ( ) {
if ( this . _ffz _installed )
return ;
this . _ffz _installed = true ;
if ( ! this . _ffzUpdateVolume )
this . _ffzUpdateVolume = debounce ( this . ffzUpdateVolume . bind ( this ) ) ;
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 ) {
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 ( ) ;
}
cls . prototype . ffzUpdateVolume = function ( ) {
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 ) ;
}
}
}
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 ;
const state = player . state ? . state ;
if ( state === 'Playing' ) {
const video = player . mediaSinkManager ? . video ;
if ( video ? . _ffz _maybe _compress ) {
video . _ffz _maybe _compress = false ;
t . compressPlayer ( this ) ;
}
}
ds . ended = state === 'Ended' ;
ds . paused = state === 'Idle' ;
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 ) ;
if ( ! this . _ffz _menu _handler )
this . _ffz _menu _handler = this . ffzMenuHandler . bind ( this ) ;
on ( cont , 'wheel' , this . _ffz _scroll _handler ) ;
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 ;
}
this . _ffz _listeners = false ;
}
cls . prototype . ffzClickHandler = function ( event ) {
if ( ! event )
return ;
if ( t . settings . get ( 'player.volume-scroll' ) === 2 && event . button === 2 ) {
this . ffz _rmb = true ;
this . ffz _scrolled = false ;
}
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 ) {
const setting = t . settings . get ( 'player.volume-scroll' ) ;
if ( ! setting )
return ;
if ( setting === 2 && ! this . ffz _rmb )
return ;
const delta = event . wheelDelta || - ( event . deltaY || event . detail || 0 ) ,
player = this . props ? . mediaPlayerInstance ,
video = player ? . mediaSinkManager ? . video || player ? . core ? . mediaSinkManager ? . video ;
if ( ! player ? . getVolume )
return ;
if ( setting === 2 )
this . ffz _scrolled = true ;
const amount = t . settings . get ( 'player.volume-scroll-steps' ) ,
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 } ) ) ;
}
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 ;
}
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 ;
return document . visibilityState === 'hidden' ;
}
} ) ;
} 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
const font _size = this . settings . get ( 'player.captions.font-size' ) ;
let font _family = this . settings . get ( 'player.captions.font-family' ) ;
if ( font _family . indexOf ( ' ' ) !== - 1 && font _family . indexOf ( ',' ) === - 1 && font _family . indexOf ( '"' ) === - 1 && font _family . indexOf ( "'" ) === - 1 )
font _family = ` " ${ font _family } " ` ;
STYLE _VALIDATOR . style . fontSize = '' ;
STYLE _VALIDATOR . style . fontFamily = '' ;
STYLE _VALIDATOR . style . fontSize = font _size ;
STYLE _VALIDATOR . style . fontFamily = font _family ;
const font _out = [ ] ;
if ( STYLE _VALIDATOR . style . fontFamily )
font _out . push ( ` font-family: ${ STYLE _VALIDATOR . style . fontFamily } !important; ` ) ;
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 ) ;
this . addMetadata ( inst ) ;
if ( inst . _ffzUpdateVolume )
inst . _ffzUpdateVolume ( ) ;
this . emit ( ':update-gui' , inst ) ;
}
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 ,
container = outer && outer . querySelector ( '.player-controls__left-control-group' ) ,
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 ;
}
let icon , tip , extra , ff _el , btn , cont = container . querySelector ( '.ffz--player-comp' ) ;
if ( ! has _comp ) {
if ( cont )
cont . remove ( ) ;
return ;
}
if ( ! cont ) {
cont = ( < div class = "ffz--player-comp tw-inline-flex tw-relative tw-tooltip__container" >
{ 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 > ) }
< div class = "tw-tooltip tw-tooltip--align-left tw-tooltip--up" role = "tooltip" >
< div >
{ tip = ( < div class = "ffz--p-tip" / > ) }
{ extra = ( < div class = "ffz--p-extra tw-pd-t-05 ffz--tooltip-explain" / > ) }
{ ff _el = IS _FIREFOX ? ( < div class = "ffz--p-ff tw-pd-t-05 ffz--tooltip-explain" / > ) : null }
< / div >
< / div >
< / div > ) ;
container . appendChild ( cont ) ;
} else if ( visible _only )
return ;
else {
icon = cont . querySelector ( 'figure' ) ;
btn = cont . querySelector ( 'button' ) ;
tip = cont . querySelector ( '.tw-tooltip .ffz--p-tip' ) ;
extra = cont . querySelector ( '.tw-tooltip .ffz--p-extra' ) ;
}
const comp _active = video . _ffz _compressed ,
can _apply = this . canCompress ( inst ) ,
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.' ) ;
extra . textContent = this . i18n . t ( 'player.comp_button.help' , 'See the FFZ Control Center for details. If audio breaks, please reset the player.' ) ;
if ( ff _el )
ff _el . textContent += ` \ n ${ this . i18n . t ( 'player.comp_button.firefox' , 'Playback Speed controls will not function for Firefox users when the Compressor has been enabled.' ) } ` ;
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 ) {
const video = inst . props . mediaPlayerInstance ? . mediaSinkManager ? . video ||
inst . props . mediaPlayerInstance ? . core ? . mediaSinkManager ? . video ;
if ( ! video || ! HAS _COMPRESSOR )
return ;
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 ;
}
if ( wanted == compressed || ( e == null && video . _ffz _toggled ) )
return ;
const ctx = video . _ffz _context ,
comp = video . _ffz _compressor ,
src = video . _ffz _source ;
if ( ! ctx || ! comp || ! src )
return ;
if ( wanted ) {
src . disconnect ( ctx . destination ) ;
src . connect ( comp ) ;
comp . connect ( ctx . destination ) ;
} else {
src . disconnect ( comp ) ;
comp . disconnect ( ctx . destination ) ;
src . connect ( ctx . destination ) ;
}
video . _ffz _compressed = wanted ;
this . addCompressorButton ( inst ) ;
}
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 ;
if ( video . src ) {
const url = new URL ( video . src ) ;
if ( url . protocol !== 'blob:' )
return false ;
} else
return false ;
return true ;
}
createCompressor ( inst , video ) {
if ( ! this . canCompress ( inst ) )
return ;
let comp = video . _ffz _compressor ;
if ( ! comp ) {
const ctx = new AudioContext ( ) ;
if ( ! IS _FIREFOX && ctx . state === 'suspended' ) {
this . log . info ( 'Aborting due to browser auto-play policy.' ) ;
return ;
}
video . _ffz _context = ctx ;
const src = video . _ffz _source = ctx . createMediaElementSource ( video ) ;
src . connect ( ctx . destination ) ;
comp = video . _ffz _compressor = ctx . createDynamicsCompressor ( ) ;
video . _ffz _compressed = false ;
}
this . updateCompressor ( null , comp ) ;
}
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 ) ,
container = outer && outer . querySelector ( '.player-controls__right-control-group' ) ,
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 ) {
cont = ( < div class = "ffz--player-pip tw-inline-flex tw-relative tw-tooltip__container" >
{ 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 > ) }
{ tip = ( < div class = "tw-tooltip tw-tooltip--align-right tw-tooltip--up" role = "tooltip" / > ) }
< / 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' ) ;
tip = cont . querySelector ( '.tw-tooltip' ) ;
}
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 ) ,
container = outer && outer . querySelector ( '.player-controls__right-control-group' ) ,
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 ) {
cont = ( < div class = "ffz--player-reset tw-inline-flex tw-relative tw-tooltip__container" >
{ 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 > ) }
{ tip = ( < div class = "tw-tooltip tw-tooltip--align-right tw-tooltip--up" role = "tooltip" / > ) }
< / 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' ) ;
tip = cont . querySelector ( '.tw-tooltip' ) ;
}
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 ) {
cont = ( < div class = "ffz--player-reset tw-absolute tw-bottom-0 tw-right-0 tw-tooltip__container tw-mg-1" >
{ 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 > ) }
{ tip = ( < div class = "tw-tooltip tw-tooltip--align-right tw-tooltip--up" role = "tooltip" / > ) }
< / div > ) ;
container . appendChild ( cont ) ;
} else {
btn = cont . querySelector ( 'button' ) ;
tip = cont . querySelector ( '.tw-tooltip' ) ;
}
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 ;
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' ) ,
vol = video ? . volume ? ? player . getVolume ( ) ,
muted = player . isMuted ( ) ;
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 ;
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 ) ,
container = outer && outer . querySelector ( '.player-controls__right-control-group' ) ;
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 ;
}
}