import Constants from './lib/constants' ; import WebFilter from './webFilter' ; import BookmarkletFilter from './bookmarkletFilter' ; import WebAudioSites from './webAudioSites' ; import { getGlobalVariable , getParent , hmsToSeconds , makeRequest , secondsToHMS } from './lib/helper' ; import Logger from './lib/logger' ; const logger = new Logger ( ) ; export default class WebAudio { cueRuleIds : number [ ] ; enabledRuleIds : number [ ] ; fetching : boolean ; fillerAudio : HTMLAudioElement ; fillerAudioPauseHandler : any ; fillerAudioPlayHandler : any ; filter : WebFilter | BookmarkletFilter ; lastFilteredNode : HTMLElement | ChildNode ; lastFilteredText : string ; lastProcessedText : string ; muted : boolean ; rules : AudioRule [ ] ; sites : { [ site ] : AudioRule [ ] ; } ; siteKey : string ; supportedPage : boolean ; unmuteTimeout : number ; volume : number ; watcherRuleIds : number [ ] ; wordlistId : number ; youTube : boolean ; youTubeAutoSubsMax : number ; youTubeAutoSubsMin : number ; youTubeAutoSubsRule : AudioRule ; youTubeAutoSubsTimeout : number ; youTubeAutoSubsUnmuteDelay : number ; static readonly brTagRegExp = new RegExp ( '<br>' , 'i' ) ; static readonly defaultVideoSelector = 'video' ; static readonly fillerConfig = { beep : { fileName : 'audio/beep.mp3' , volume : 0.2 , } , crickets : { fileName : 'audio/crickets.mp3' , volume : 0.4 , } , static : { fileName : 'audio/static.mp3' , volume : 0.3 , } , } ; static readonly textTrackRuleMappings = { externalSubTrackLabel : 'label' , videoCueKind : 'kind' , videoCueLabel : 'label' , videoCueLanguage : 'language' , } ; constructor ( filter ) { this . filter = filter ; this . cueRuleIds = [ ] ; this . enabledRuleIds = [ ] ; this . watcherRuleIds = [ ] ; if ( this . filter . extension ) { this . fillerAudio = this . initFillerAudio ( this . filter . cfg . fillerAudio ) ; } this . lastFilteredNode = null ; this . lastFilteredText = '' ; this . lastProcessedText = '' ; this . muted = false ; if ( ! filter . cfg . customAudioSites || typeof filter . cfg . customAudioSites !== 'object' ) { filter . cfg . customAudioSites = { } ; } this . sites = WebAudioSites . combineSites ( filter . cfg . customAudioSites ) ; this . volume = 1 ; this . wordlistId = filter . audioWordlistId ; this . youTubeAutoSubsMax = filter . cfg . youTubeAutoSubsMax * 1000 ; this . youTubeAutoSubsMin = filter . cfg . youTubeAutoSubsMin ; this . youTubeAutoSubsUnmuteDelay = 0 ; this . siteKey = this . getSiteKey ( ) ; this . rules = this . sites [ this . siteKey ] ; if ( this . rules ) { if ( ! Array . isArray ( this . rules ) ) { this . rules = [ this . rules ] ; } this . rules . forEach ( ( rule ) => { this . initRule ( rule ) ; } ) ; if ( this . enabledRuleIds . length > 0 ) { this . supportedPage = true ; this . initYouTube ( ) ; } } } apfCaptionLine ( rule , text ) { const line = document . createElement ( 'span' ) ; line . classList . add ( 'APF-subtitle-line' ) ; line . style . background = 'black' ; line . style . color = 'white' ; line . style . fontSize = '3.5vw' ; line . style . paddingLeft = '4px' ; line . style . paddingRight = '4px' ; line . style . height = '18px' ; line . textContent = text ; return line ; } apfCaptionLines ( rule , lines ) { const apfLines = document . createElement ( 'div' ) ; apfLines . classList . add ( 'APF-subtitles' ) ; apfLines . style . bottom = '10px' ; apfLines . style . position = 'absolute' ; apfLines . style . textAlign = 'center' ; apfLines . style . width = '100%' ; lines . forEach ( ( line ) => { apfLines . appendChild ( line ) ; apfLines . appendChild ( document . createElement ( 'br' ) ) ; } ) ; return apfLines ; } clean ( subtitleContainer , ruleIndex = 0 ) { const rule = this . rules [ ruleIndex ] ; if ( rule . mode === 'watcher' ) { return ; } let filtered = false ; if ( subtitleContainer . nodeName && subtitleContainer . nodeName === '#text' && subtitleContainer . parentElement ) { subtitleContainer = subtitleContainer . parentElement ; } const subtitles = rule . subtitleSelector && subtitleContainer . querySelectorAll ? subtitleContainer . querySelectorAll ( rule . subtitleSelector ) : [ subtitleContainer ] ; if ( subtitles . length === 0 ) { return ; } subtitles . forEach ( ( subtitle ) => { const textMethod = subtitle . nodeName === '#text' ? 'textContent' : 'innerText' ; if ( rule . convertBreaks === true && subtitle . nodeName !== '#text' && ! WebAudio . brTagRegExp . test ( subtitle [ textMethod ] ) && WebAudio . brTagRegExp . test ( subtitle . innerHTML ) ) { if ( subtitle . style . whiteSpace !== 'pre' ) { subtitle . style . whiteSpace = 'pre' ; } subtitle . textContent = subtitle . innerHTML . replace ( WebAudio . brTagRegExp , '\n' ) ; } const result = this . replaceTextResult ( subtitle [ textMethod ] ) ; if ( result . modified ) { filtered = true ; this . mute ( rule ) ; if ( rule . filterSubtitles ) { if ( rule . preserveWhiteSpace && subtitle . style . whiteSpace !== 'pre' ) { subtitle . style . whiteSpace = 'pre' ; } if ( rule . ignoreMutations ) { this . filter . stopObserving ( ) ; } subtitle [ textMethod ] = result . filtered ; if ( rule . ignoreMutations ) { this . filter . startObserving ( ) ; } } this . lastFilteredNode = subtitle ; this . lastFilteredText = subtitle [ textMethod ] ; } if ( ! filtered && this . lastFilteredNode == subtitle && this . lastFilteredText == subtitle [ textMethod ] ) { filtered = true ; } } ) ; if ( ! filtered ) { if ( this . lastFilteredNode && document . body . contains ( this . lastFilteredNode ) && this . lastFilteredNode . textContent === this . lastFilteredText ) { filtered = true ; } } const shouldBeShown = this . subtitlesShouldBeShown ( rule , filtered ) ; shouldBeShown ? this . showSubtitles ( rule , subtitles ) : this . hideSubtitles ( rule , subtitles ) ; } cleanYouTubeAutoSubs ( node ) { if ( this . youTubeAutoSubsTimeout != null ) { clearTimeout ( this . youTubeAutoSubsTimeout ) ; this . youTubeAutoSubsTimeout = null ; } const result = this . replaceTextResult ( node . textContent ) ; if ( result . modified ) { if ( this . youTubeAutoSubsRule . filterSubtitles ) { node . textContent = result . filtered ; } this . mute ( this . youTubeAutoSubsRule ) ; this . youTubeAutoSubsUnmuteDelay = null ; this . filter . updateCounterBadge ( ) ; if ( this . youTubeAutoSubsMax ) { this . youTubeAutoSubsTimeout = window . setTimeout ( this . youTubeAutoSubsMuteTimeout , this . youTubeAutoSubsMax , this ) ; } } else { if ( this . muted ) { if ( this . youTubeAutoSubsMin > 0 ) { const currentTime = document . getElementsByTagName ( WebAudio . defaultVideoSelector ) [ 0 ] . currentTime ; if ( this . youTubeAutoSubsUnmuteDelay == null ) { this . youTubeAutoSubsUnmuteDelay = currentTime ; } else { if ( currentTime < this . youTubeAutoSubsUnmuteDelay ) { this . youTubeAutoSubsUnmuteDelay = 0 ; } if ( currentTime > ( this . youTubeAutoSubsUnmuteDelay + this . youTubeAutoSubsMin ) ) { this . unmute ( this . youTubeAutoSubsRule ) ; } } } else { this . unmute ( this . youTubeAutoSubsRule ) ; } } } if ( this . filter . cfg . showSubtitles !== Constants . SHOW_SUBTITLES . ALL ) { const container = document . querySelector ( 'div.ytp-caption-window-rollup span.captions-text' ) as HTMLElement ; if ( container . style . display == 'block' ) { container . style . display = 'none' ; } } } clearUnmuteTimeout ( rule ) { if ( rule . unmuteDelay && this . unmuteTimeout != null ) { clearTimeout ( this . unmuteTimeout ) ; this . unmuteTimeout = null ; } } delayedUnmute ( instance , rule ) { const delayed = true ; instance . unmute ( rule , null , delayed ) ; this . unmuteTimeout = null ; } fillerAudioHandlePause ( ) { this . fillerAudio . pause ( ) ; } fillerAudioHandlePlay ( ) { if ( this . muted ) { this . fillerAudio . play ( ) ; } } getSiteKey ( ) { if ( this . sites . hasOwnProperty ( this . filter . hostname ) ) { return this . filter . hostname ; } else if ( this . filter . iframe && this . filter . iframe . hostname && this . sites . hasOwnProperty ( this . filter . iframe . hostname ) ) { return this . filter . iframe . hostname ; } return '' ; } getVideoTextTrack ( textTracks , rule , overrideKey ? ) { let bestIndex = 0 ; let bestScore = 0 ; let foundCues = false ; let perfectScore = 0 ; if ( overrideKey && rule [ overrideKey ] ) { perfectScore += 1000 ; } if ( rule . videoCueLabel ) { perfectScore += 100 ; } if ( rule . videoCueLanguage ) { perfectScore += 10 ; } if ( rule . videoCueKind ) { perfectScore += 1 ; } for ( let i = 0 ; i < textTracks . length ; i ++ ) { const textTrack = textTracks [ i ] ; if ( textTrack . cues . length === 0 ) { continue ; } if ( rule . videoCueRequireShowing && textTrack . mode !== 'showing' ) { continue ; } let currentScore = 0 ; if ( overrideKey && rule [ overrideKey ] && this . textTrackKeyTest ( textTrack , WebAudio . textTrackRuleMappings [ overrideKey ] , rule [ overrideKey ] ) ) { currentScore += 1000 ; } if ( rule . videoCueLabel && this . textTrackKeyTest ( textTrack , WebAudio . textTrackRuleMappings . videoCueLabel , rule . videoCueLabel ) ) { currentScore += 100 ; } if ( rule . videoCueLanguage && this . textTrackKeyTest ( textTrack , WebAudio . textTrackRuleMappings . videoCueLanguage , rule . videoCueLanguage ) ) { currentScore += 10 ; } if ( rule . videoCueKind ) { if ( this . textTrackKeyTest ( textTrack , WebAudio . textTrackRuleMappings . videoCueKind , rule . videoCueKind ) ) { currentScore += 1 ; } } else { if ( this . textTrackKeyTest ( textTrack , WebAudio . textTrackRuleMappings . videoCueKind , 'captions' ) || this . textTrackKeyTest ( textTrack , WebAudio . textTrackRuleMappings . videoCueKind , 'subtitles' ) ) { currentScore += 1 ; } } if ( currentScore === perfectScore ) { return textTrack ; } if ( currentScore > bestScore || ! foundCues ) { bestScore = currentScore ; bestIndex = i ; foundCues = true ; } } if ( foundCues ) { return textTracks [ bestIndex ] ; } } hideCue ( rule , cue ) { cue . text = '' ; cue . position = 100 ; cue . size = 0 ; } hideSubtitles ( rule , subtitles ? ) { if ( rule . displayVisibility && rule . _displayElement ) { rule . _displayElement . style . visibility = 'hidden' ; } else if ( rule . displaySelector ) { const root = rule . rootNode && subtitles && subtitles [ 0 ] ? subtitles [ 0 ] . getRootNode ( ) : document ; if ( root ) { const container = root . querySelector ( rule . displaySelector ) as HTMLElement ; if ( container ) { if ( rule . displayShow === '' && container . style . display !== '' && container . style . display !== rule . displayHide ) { rule . displayShow = container . style . display ; } container . style . setProperty ( 'display' , rule . displayHide ) ; } } } else if ( subtitles ) { subtitles . forEach ( ( subtitle ) => { subtitle . innerText = '' ; if ( rule . removeSubtitleSpacing && subtitle . style ) { if ( subtitle . style . padding ) { subtitle . style . padding = 0 ; } if ( subtitle . style . margin ) { subtitle . style . margin = 0 ; } } } ) ; } } initCueRule ( rule ) { if ( rule . apfCaptions === true ) { rule . videoCueHideCues = true ; } if ( rule . videoSelector === undefined ) { rule . videoSelector = WebAudio . defaultVideoSelector ; } if ( rule . videoCueRequireShowing === undefined ) { rule . videoCueRequireShowing = this . filter . cfg . muteCueRequireShowing ; } if ( rule . externalSub ) { if ( rule . externalSubTrackMode === undefined ) { rule . externalSubTrackMode = 'showing' ; } if ( rule . externalSubURLKey === undefined ) { rule . externalSubURLKey = 'url' ; } if ( rule . externalSubFormatKey === undefined ) { rule . externalSubFormatKey = 'format' ; } if ( rule . externalSubTrackLabel === undefined ) { rule . externalSubTrackLabel = 'APF' ; } } } initDisplaySelector ( rule ) { if ( rule . displaySelector !== undefined ) { if ( rule . displayHide === undefined ) { rule . displayHide = 'none' ; } if ( rule . displayShow === undefined ) { rule . displayShow = '' ; } } } initDynamicRule ( rule ) { rule . _dynamic = true ; if ( rule . dynamicTargetMode == undefined ) { rule . disabled == true ; } } initElementChildRule ( rule ) { if ( ! rule . parentSelector && ! rule . parentSelectorAll ) { rule . disabled = true ; } } initElementRule ( rule ) { } initFillerAudio ( name = '' ) { const fillerConfig = WebAudio . fillerConfig [ name ] ; if ( fillerConfig ) { const url = chrome . runtime . getURL ( fillerConfig . fileName ) ; const audioFiller = new Audio ( ) ; audioFiller . src = url ; audioFiller . loop = true ; if ( fillerConfig . volume ) { audioFiller . volume = fillerConfig . volume ; } if ( fillerConfig . loopAfter ) { audioFiller . ontimeupdate = ( ) => { if ( audioFiller . currentTime > fillerConfig . loopAfter ) { audioFiller . currentTime = 0 ; } } ; } this . fillerAudioPauseHandler = this . fillerAudioHandlePause . bind ( this ) ; this . fillerAudioPlayHandler = this . fillerAudioHandlePlay . bind ( this ) ; return audioFiller ; } } initRule ( rule ) { const ruleId = this . rules . indexOf ( rule ) ; if ( rule . mode === undefined || ( ( rule . mode == 'element' || rule . mode == 'elementChild' ) && ! rule . tagName ) || ( rule . iframe === true && this . filter . iframe == null ) || ( rule . iframe === false && this . filter . iframe != null ) ) { rule . disabled = true ; } if ( ! rule . disabled ) { if ( rule . filterSubtitles == null ) { rule . filterSubtitles = true ; } if ( this . filter . filterText == false ) { rule . filterSubtitles = false ; } this . initDisplaySelector ( rule ) ; if ( rule . muteMethod == null ) { rule . muteMethod = this . filter . cfg . muteMethod ; } if ( rule . showSubtitles == null ) { rule . showSubtitles = this . filter . cfg . showSubtitles ; } if ( rule . tagName != null && rule . tagName != '#text' ) { rule . tagName = rule . tagName . toUpperCase ( ) ; } switch ( rule . mode ) { case 'cue' : this . initCueRule ( rule ) ; if ( ! rule . disabled ) { this . cueRuleIds . push ( ruleId ) ; } break ; case 'dynamic' : this . initDynamicRule ( rule ) ; break ; case 'elementChild' : this . initElementChildRule ( rule ) ; break ; case 'element' : this . initElementRule ( rule ) ; break ; case 'text' : this . initTextRule ( rule ) ; break ; case 'watcher' : this . initWatcherRule ( rule ) ; if ( ! rule . disabled ) { this . watcherRuleIds . push ( ruleId ) ; } break ; } if ( ! rule . disabled ) { this . enabledRuleIds . push ( ruleId ) ; if ( rule . mode == 'cue' && this . cueRuleIds . length === 1 ) { setInterval ( this . watchForVideo , 250 , this ) ; } else if ( rule . mode == 'watcher' ) { setInterval ( this . watcher , rule . checkInterval , this , ruleId ) ; } } } } initTextRule ( rule ) { rule . tagName = '#text' ; if ( rule . simpleUnmute === undefined ) { rule . simpleUnmute = true ; } } initWatcherRule ( rule ) { if ( rule . checkInterval === undefined ) { rule . checkInterval = 20 ; } if ( rule . ignoreMutations === undefined ) { rule . ignoreMutations = true ; } if ( rule . simpleUnmute === undefined ) { rule . simpleUnmute = true ; } if ( rule . videoSelector === undefined ) { rule . videoSelector = WebAudio . defaultVideoSelector ; } } initYouTube ( ) { if ( [ 'm.youtube.com' , 'tv.youtube.com' , 'www.youtube.com' ] . includes ( this . siteKey ) ) { this . youTube = true ; const youTubeAutoCensor = '[ __ ]' ; const lists = this . wordlistId === Constants . ALL_WORDS_WORDLIST_ID ? [ ] : [ this . wordlistId ] ; const youTubeAutoCensorOptions = { lists : lists , matchMethod : Constants . MATCH_METHODS . PARTIAL , repeat : Constants . FALSE , separators : Constants . FALSE , sub : '' , } ; this . filter . cfg . addWord ( youTubeAutoCensor , youTubeAutoCensorOptions ) ; this . youTubeAutoSubsRule = { filterSubtitles : true , mode : 'ytauto' , muteMethod : this . filter . cfg . muteMethod } as AudioRule ; } } mute ( rule ? , video ? ) { if ( ! this . muted ) { this . muted = true ; if ( this . filter . cfg . collectStats ) { this . filter . stats . mutes ++ ; } switch ( rule . muteMethod ) { case Constants . MUTE_METHODS . TAB : chrome . runtime . sendMessage ( { mute : true } ) ; break ; case Constants . MUTE_METHODS . VIDEO_MUTE : if ( ! video ) { video = document . querySelector ( rule && rule . videoSelector ? rule . videoSelector : WebAudio . defaultVideoSelector ) ; } if ( video && ! video . muted ) { video . muted = true ; } if ( this . fillerAudio ) { this . playFillerAudio ( video ) ; } break ; case Constants . MUTE_METHODS . VIDEO_VOLUME : if ( ! video ) { video = document . querySelector ( rule && rule . videoSelector ? rule . videoSelector : WebAudio . defaultVideoSelector ) ; } if ( video && video . volume != null ) { this . volume = video . volume ; video . volume = 0 ; } if ( this . fillerAudio ) { this . playFillerAudio ( video ) ; } break ; } } if ( rule && rule . unmuteDelay && this . unmuteTimeout ) { this . clearUnmuteTimeout ( rule ) ; } } newCue ( start , end , text , options = { } ) { try { const cue = new VTTCue ( hmsToSeconds ( start ) , hmsToSeconds ( end ) , text ) ; if ( options . align ) { cue . align = options . align ; } if ( options . line ) { cue . line = this . parseLineAndPositionSetting ( options . line ) ; } if ( options . position ) { cue . position = this . parseLineAndPositionSetting ( options . position ) ; } return cue ; } catch ( e ) { logger . error ( `[Audio] Failed to add cue: ( start: ${ start }, end: ${ end }, text: ${ text } )` , e ) ; } } newTextTrack ( rule , video , cues ) { if ( video . textTracks ) { const track = video . addTextTrack ( 'captions' , rule . externalSubTrackLabel , rule . videoCueLanguage ) as TextTrack ; track . mode = rule . externalSubTrackMode ; for ( let i = 0 ; i < cues . length ; i ++ ) { track . addCue ( cues [ i ] ) ; } return track ; } } parseLineAndPositionSetting ( setting ) { if ( typeof setting == 'string' && setting != '' ) { if ( setting == 'auto' ) { return 'auto' ; } else { return parseInt ( setting ) ; } } } parseSRT ( srt ) { const lines = srt . trim ( ) . replace ( '\r\n' , '\n' ) . split ( /[\r\n]/ ) . map ( ( line ) => line . trim ( ) ) ; const cues = [ ] ; let start = null ; let end = null ; let text = null ; for ( let i = 0 ; i < lines . length ; i ++ ) { if ( lines [ i ] . indexOf ( '-->' ) >= 0 ) { const splitted = lines [ i ] . split ( /[ \t]+-->[ \t]+/ ) ; if ( splitted . length != 2 ) { throw 'Error when splitting "-->": ' + lines [ i ] ; } start = splitted [ 0 ] ; end = splitted [ 1 ] ; } else if ( lines [ i ] == '' ) { if ( start && end ) { const cue = this . newCue ( start , end , text ) ; cues . push ( cue ) ; start = null ; end = null ; text = null ; } } else if ( start && end ) { if ( text == null ) { text = lines [ i ] ; } else { text += '\n' + lines [ i ] ; } } } if ( start && end ) { const cue = this . newCue ( start , end , text ) ; cues . push ( cue ) ; } return cues ; } parseSSA ( ssa ) { const cues = [ ] ; let endIndex , startIndex , textIndex ; let foundEvents = false ; const lines = ssa . split ( '\n' ) ; for ( let i = 0 ; i < lines . length ; i ++ ) { if ( ! foundEvents ) { if ( lines [ i ] . match ( /^\[Events\]/i ) ) { foundEvents = true ; } continue ; } if ( lines [ i ] . match ( /^format:/i ) ) { const format = lines [ i ] . trim ( ) . split ( ',' ) ; endIndex = format . indexOf ( 'End' ) ; startIndex = format . indexOf ( 'Start' ) ; textIndex = format . indexOf ( 'Text' ) ; } else if ( lines [ i ] . match ( /^dialogue:/i ) ) { const line = lines [ i ] . trim ( ) . split ( ',' ) ; const start = line [ startIndex ] ; const end = line [ endIndex ] ; const cleanText = line . slice ( textIndex ) . join ( ',' ) . replace ( /\{\\\w.+?\}/g , '' ) . split ( '\\N' ) . reverse ( ) ; for ( let j = 0 ; j < cleanText . length ; j ++ ) { cues . push ( this . newCue ( start , end , cleanText [ j ] ) ) ; } } } return cues ; } parseVTT ( input ) { const cues = [ ] ; const lines = input . split ( '\n' ) ; const separator = new RegExp ( '\\s-->\\s' ) ; for ( let i = 0 ; i < lines . length ; i ++ ) { const line = lines [ i ] . trim ( ) ; if ( line . match ( separator ) ) { const parts = line . replace ( separator , ' ' ) . split ( ' ' ) ; let [ start , end , ... extraOptions ] = parts ; start = start . replace ( ',' , '.' ) ; end = end . replace ( ',' , '.' ) ; const options = extraOptions . map ( ( o ) => o . split ( ':' ) ) . reduce ( ( acc , cur ) => { acc [ cur [ 0 ] ] = cur [ 1 ] ; return acc ; } , { } ) ; const prevLine = lines [ i - 1 ] . trim ( ) ; const nextLine = lines [ i + 1 ] . trim ( ) ; const textStartRegex = new RegExp ( `^<[cs]\\.${ prevLine }>` ) ; const textEndRegex = new RegExp ( '<\/[cs]>$' ) ; let text ; if ( nextLine . match ( textStartRegex ) ) { text = nextLine . replace ( textStartRegex , '' ) . replace ( textEndRegex , '' ) ; } else { text = nextLine ; } if ( prevLine && ! prevLine . match ( /_1$/ ) ) { const previousCue = cues [ cues . length - 1 ] ; if ( previousCue . startTime != hmsToSeconds ( start ) && previousCue . endTime == hmsToSeconds ( end ) ) { start = secondsToHMS ( previousCue . startTime ) ; } } const cue = this . newCue ( start , end , text , options ) ; if ( prevLine && ! prevLine . match ( /_1$/ ) ) { const concurrentNumber = parseInt ( prevLine . match ( /_([2-9])$/ ) [ 1 ] ) ; const firstConcurrentCueIndex = ( cues . length - concurrentNumber ) + 1 ; cues . splice ( firstConcurrentCueIndex , 0 , cue ) ; } else { cues . push ( cue ) ; } i ++ ; } } return cues ; } playFillerAudio ( video ) { if ( this . playing ( video ) ) { this . fillerAudio . play ( ) ; video . addEventListener ( 'pause' , this . fillerAudioPauseHandler ) ; video . addEventListener ( 'play' , this . fillerAudioPlayHandler ) ; } } playing ( video ) { return ! ! ( video && video . currentTime > 0 && ! video . paused && ! video . ended && video . readyState > 2 ) ; } processCues ( cues , rule ) { for ( let i = 0 ; i < cues . length ; i ++ ) { const cue = cues [ i ] ; if ( cue . hasOwnProperty ( 'filtered' ) ) { continue ; } if ( rule . videoCueSync ) { cue . startTime += rule . videoCueSync ; cue . endTime += rule . videoCueSync ; } const result = this . replaceTextResult ( cue . text ) ; cue . originalText = cue . text ; if ( result . modified ) { cue . filtered = true ; if ( rule . filterSubtitles ) { cue . text = result . filtered ; } } else { cue . filtered = false ; } } } async processExternalSub ( video , rule ) { const textTrack = this . getVideoTextTrack ( video . textTracks , rule , 'externalSubTrackLabel' ) ; if ( ! this . fetching && ! textTrack ) { try { const subsData = getGlobalVariable ( rule . externalSubVar ) ; if ( Array . isArray ( subsData ) ) { const found = subsData . find ( ( subtitle ) => subtitle . language === rule . videoCueLanguage ) ; if ( ! found ) { throw ( `Failed to find subtitle for language: ${ rule . videoCueLanguage }.` ) ; } this . fetching = true ; const subs = await makeRequest ( 'GET' , found [ rule . externalSubURLKey ] ) as string ; if ( typeof subs == 'string' && subs ) { let parsedCues ; switch ( found [ rule . externalSubFormatKey ] ) { case 'ass' : parsedCues = this . parseSSA ( subs ) ; break ; case 'srt' : parsedCues = this . parseSRT ( subs ) ; break ; case 'vtt' : parsedCues = this . parseVTT ( subs ) ; break ; default : throw ( `Unsupported subtitle type: ${ found [ rule . externalSubFormatKey ] }` ) ; } if ( parsedCues ) { const track = this . newTextTrack ( rule , video , parsedCues ) ; const cues = track . cues as any as FilteredVTTCue [ ] ; this . processCues ( cues , rule ) ; this . fetching = false ; if ( rule . displaySelector ) { const oldSubtitlesContainer = document . querySelector ( rule . displaySelector ) as HTMLElement ; if ( oldSubtitlesContainer ) { oldSubtitlesContainer . style . display = 'none' ; } } } } else { throw ( `Failed to download external subtitles from '${ found [ rule . externalSubURLKey ] }'.` ) ; } } else { throw ( `Failed to find subtitle variable: ${ rule . externalSubVar }` ) ; } } catch ( e ) { logger . error ( `[Audio] Error using external subtitles for ${ this . siteKey }.` , e ) ; } } } processWatcherCaptions ( rule , captions , data ) { const initialCall = data . initialCall ; if ( initialCall ) { if ( this . lastProcessedText && this . lastProcessedText === captions . textContent ) { data . skipped = true ; return false ; } else { this . unmute ( rule ) ; this . lastProcessedText = '' ; } data . initialCall = false ; data . filtered = false ; } if ( captions . hasChildNodes ( ) ) { captions . childNodes . forEach ( ( child ) => { this . processWatcherCaptions ( rule , child , data ) ; } ) ; } else { const textMethod = ( captions && captions . nodeName ) === '#text' ? 'textContent' : 'innerText' ; if ( captions [ textMethod ] && captions [ textMethod ] . trim ( ) ) { const result = this . replaceTextResult ( captions [ textMethod ] ) ; if ( result . modified ) { this . mute ( rule ) ; data . filtered = true ; if ( rule . filterSubtitles ) { captions [ textMethod ] = result . filtered ; } } } } if ( initialCall ) { this . lastProcessedText = captions . textContent ; } } processWatcherCaptionsArray ( rule , captions , data ) { const originalText = captions . map ( ( caption ) => caption . textContent ) . join ( ' ' ) ; if ( this . lastProcessedText && this . lastProcessedText === originalText ) { data . skipped = true ; return false ; } else { this . unmute ( rule ) ; this . lastProcessedText = '' ; data . filtered = false ; } captions . forEach ( ( caption ) => { if ( caption . textContent && caption . textContent . trim ( ) ) { const result = this . replaceTextResult ( caption . textContent ) ; if ( result . modified ) { this . mute ( rule ) ; data . filtered = true ; if ( rule . filterSubtitles ) { caption . textContent = result . filtered ; } } } } ) ; const shouldBeShown = this . subtitlesShouldBeShown ( rule , data . filtered ) ; shouldBeShown ? this . showSubtitles ( rule ) : this . hideSubtitles ( rule ) ; this . lastProcessedText = captions . map ( ( caption ) => caption . textContent ) . join ( ' ' ) ; } replaceTextResult ( string , wordlistId = this . wordlistId , statsType = Constants . STATS_TYPE_AUDIO ) { return this . filter . replaceTextResult ( string , wordlistId , statsType ) ; } showSubtitles ( rule , subtitles ? ) { if ( rule . displayVisibility && rule . _displayElement ) { rule . _displayElement . style . visibility = 'visible' ; } else if ( rule . displaySelector ) { const root = rule . rootNode && subtitles && subtitles [ 0 ] ? subtitles [ 0 ] . getRootNode ( ) : document ; if ( root ) { const container = root . querySelector ( rule . displaySelector ) ; if ( container ) { container . style . setProperty ( 'display' , rule . displayShow ) ; } } } } stopFillerAudio ( ) { this . fillerAudio . pause ( ) ; this . fillerAudio . currentTime = 0 ; } subtitlesShouldBeShown ( rule , filtered = false ) { switch ( rule . showSubtitles ) { case Constants . SHOW_SUBTITLES . ALL : return true ; case Constants . SHOW_SUBTITLES . FILTERED : return filtered ; case Constants . SHOW_SUBTITLES . UNFILTERED : return ! filtered ; case Constants . SHOW_SUBTITLES . NONE : return false ; } } supportedNode ( node ) { for ( let i = 0 ; i < this . enabledRuleIds . length ; i ++ ) { const ruleId = this . enabledRuleIds [ i ] ; const rule = this . rules [ ruleId ] ; switch ( rule . mode ) { case 'element' : if ( node . nodeName == rule . tagName ) { let failed = false ; if ( ! failed && rule . className && ( ! node . className || ! node . classList . contains ( rule . className ) ) ) { failed = true ; } if ( ! failed && rule . dataPropPresent && ( ! node . dataset || ! node . dataset . hasOwnProperty ( rule . dataPropPresent ) ) ) { failed = true ; } if ( ! failed && rule . hasChildrenElements && ( typeof node . childElementCount !== 'number' || node . childElementCount == 0 ) ) { failed = true ; } if ( ! failed && rule . subtitleSelector && ( typeof node . querySelector !== 'function' || ! node . querySelector ( rule . subtitleSelector ) ) ) { failed = true ; } if ( ! failed && rule . containsSelector && ( typeof node . querySelector !== 'function' || ! node . querySelector ( rule . containsSelector ) ) ) { failed = true ; } if ( ! failed ) { return ruleId ; } } break ; case 'elementChild' : if ( node . nodeName === rule . tagName ) { const root = rule . rootNode ? node . getRootNode ( ) : document ; if ( root ) { if ( rule . parentSelector ) { const parent = root . querySelector ( rule . parentSelector ) ; if ( parent && parent . contains ( node ) ) { return ruleId ; } } else { const parents = root . querySelectorAll ( rule . parentSelectorAll ) ; for ( let j = 0 ; j < parents . length ; j ++ ) { if ( parents [ j ] . contains ( node ) ) { return ruleId ; } } } } } break ; case 'text' : if ( node . nodeName === rule . tagName ) { const parent = document . querySelector ( rule . parentSelector ) ; if ( parent && parent . contains ( node ) ) { return ruleId ; } } break ; case 'watcher' : if ( node . parentElement && node . parentElement == document . querySelector ( rule . subtitleSelector ) ) { return ruleId ; } if ( rule . parentSelector != null ) { const parent = document . querySelector ( rule . parentSelector ) ; if ( parent && parent . contains ( node ) ) { return ruleId ; } } break ; case 'dynamic' : if ( node . textContent === rule . dynamicTextKey ) { rule . mode = rule . dynamicTargetMode ; rule . parentSelectorAll = `${ node . tagName . toLowerCase ( ) }.${ Array . from ( node . classList ) . join ( '.' ) } ${ rule . parentSelectorAll }` ; this . initRule ( rule ) ; } break ; } } return false ; } textTrackKeyTest ( textTrack , key , value ) { return ( textTrack [ key ] && value && textTrack [ key ] === value ) ; } unmute ( rule ? , video ? , delayed = false ) { if ( this . muted ) { if ( ! delayed && rule && rule . unmuteDelay >= 0 ) { if ( this . unmuteTimeout == null ) { this . clearUnmuteTimeout ( rule ) ; } this . unmuteTimeout = window . setTimeout ( this . delayedUnmute , rule . unmuteDelay , this , rule ) ; return ; } this . muted = false ; switch ( rule . muteMethod ) { case Constants . MUTE_METHODS . TAB : chrome . runtime . sendMessage ( { mute : false } ) ; break ; case Constants . MUTE_METHODS . VIDEO_MUTE : if ( this . fillerAudio ) { this . stopFillerAudio ( ) ; } if ( ! video ) { video = document . querySelector ( rule && rule . videoSelector ? rule . videoSelector : WebAudio . defaultVideoSelector ) ; } if ( video && video . muted ) { video . muted = false ; } break ; case Constants . MUTE_METHODS . VIDEO_VOLUME : if ( this . fillerAudio ) { this . stopFillerAudio ( ) ; } if ( ! video ) { video = document . querySelector ( rule && rule . videoSelector ? rule . videoSelector : WebAudio . defaultVideoSelector ) ; } if ( video && video . volume != null ) { video . volume = this . volume ; } break ; } } } watcher ( instance , ruleId = 0 ) { const rule = instance . rules [ ruleId ] ; const video = document . querySelector ( rule . videoSelector ) as HTMLVideoElement ; if ( video && instance . playing ( video ) ) { if ( rule . ignoreMutations ) { instance . filter . stopObserving ( ) ; } const data = { initialCall : true } ; let captions ; let parents ; if ( rule . parentSelectorAll ) { if ( rule . _dynamic ) { parents = Array . from ( document . querySelectorAll ( rule . parentSelectorAll ) ) . filter ( ( result ) => { return result . textContent !== rule . dynamicTextKey ; } ) as HTMLElement [ ] ; if ( rule . displayVisibility && ( ! rule . _displayElement || ! document . body . contains ( rule . _displayElement ) ) ) { rule . _displayElement = getParent ( parents [ 0 ] , rule . getParentLevel ) ; } } else { parents = Array . from ( document . querySelectorAll ( rule . parentSelectorAll ) ) as HTMLElement [ ] ; } captions = parents . map ( ( parent ) => parent . querySelector ( rule . subtitleSelector ) ) ; if ( captions . length ) { instance . processWatcherCaptionsArray ( rule , captions , data ) ; } else { instance . watcherSimpleUnmute ( rule , video ) ; } } else if ( rule . parentSelector ) { captions = document . querySelector ( rule . parentSelector ) as HTMLElement ; if ( captions && captions . textContent && captions . textContent . trim ( ) ) { instance . processWatcherCaptions ( rule , captions , data ) ; } else { instance . watcherSimpleUnmute ( rule , video ) ; } } else if ( rule . subtitleSelector ) { captions = Array . from ( document . querySelectorAll ( rule . subtitleSelector ) ) as HTMLElement [ ] ; if ( captions && captions . length ) { if ( rule . displayVisibility && ( ! rule . _displayElement || ! document . body . contains ( rule . _displayElement ) ) ) { rule . _displayElement = getParent ( captions [ 0 ] , rule . displayElementLevels ) ; } instance . processWatcherCaptionsArray ( rule , captions , data ) ; } else { instance . watcherSimpleUnmute ( rule , video ) ; } } if ( data . skipped ) { return false ; } const shouldBeShown = instance . subtitlesShouldBeShown ( rule , data . filtered ) ; shouldBeShown ? instance . showSubtitles ( rule ) : instance . hideSubtitles ( rule ) ; if ( data . filtered ) { instance . filter . updateCounterBadge ( ) ; } } else { if ( rule . ignoreMutations ) { instance . filter . startObserving ( ) ; } } } watchForVideo ( instance ) { for ( let x = 0 ; x < instance . cueRuleIds . length ; x ++ ) { const rule = instance . rules [ x ] as AudioRule ; const video = document . querySelector ( rule . videoSelector ) as HTMLVideoElement ; if ( video && video . textTracks && instance . playing ( video ) ) { if ( rule . externalSub ) { instance . processExternalSub ( video , rule ) ; } const textTrack = instance . getVideoTextTrack ( video . textTracks , rule ) ; if ( textTrack && ! textTrack . oncuechange ) { if ( ! rule . videoCueHideCues && rule . showSubtitles === Constants . SHOW_SUBTITLES . NONE ) { textTrack . mode = 'hidden' ; } textTrack . oncuechange = ( ) => { if ( textTrack . activeCues && textTrack . activeCues . length > 0 ) { const activeCues = Array . from ( textTrack . activeCues as any as FilteredVTTCue [ ] ) ; const apfLines = [ ] ; const processed = activeCues . some ( ( activeCue ) => activeCue . hasOwnProperty ( 'filtered' ) ) ; if ( ! processed ) { const allCues = Array . from ( textTrack . cues as any as FilteredVTTCue [ ] ) ; instance . processCues ( allCues , rule ) ; } const filtered = activeCues . some ( ( activeCue ) => activeCue . filtered ) ; filtered ? instance . mute ( rule , video ) : instance . unmute ( rule , video ) ; const shouldBeShown = instance . subtitlesShouldBeShown ( rule , filtered ) ; for ( let i = 0 ; i < activeCues . length ; i ++ ) { const activeCue = activeCues [ i ] ; if ( ! shouldBeShown && rule . videoCueHideCues ) { instance . hideCue ( rule , activeCue ) ; } if ( rule . apfCaptions ) { const text = filtered ? activeCue . text : activeCue . originalText ; const line = instance . apfCaptionLine ( rule , text ) ; apfLines . unshift ( line ) ; } } if ( apfLines . length ) { const container = document . getElementById ( rule . apfCaptionsSelector ) ; const oldLines = container . querySelector ( 'div.APF-subtitles' ) ; if ( oldLines ) { oldLines . remove ( ) ; } if ( shouldBeShown ) { const apfCaptions = instance . apfCaptionLines ( rule , apfLines ) ; container . appendChild ( apfCaptions ) ; } } if ( ! rule . videoCueHideCues ) { textTrack . mode = shouldBeShown ? 'showing' : 'hidden' ; } if ( rule . displaySelector ) { apfLines . length || ! shouldBeShown ? instance . hideSubtitles ( rule ) : instance . showSubtitles ( rule ) ; } } else { instance . unmute ( rule , video ) ; } } ; const allCues = Array . from ( textTrack . cues as any as FilteredVTTCue [ ] ) ; instance . processCues ( allCues , rule ) ; } } } } watcherSimpleUnmute ( rule , video ) { this . unmute ( rule , video ) ; if ( rule . showSubtitles > Constants . SHOW_SUBTITLES . ALL ) { this . hideSubtitles ( rule ) ; } } youTubeAutoSubsCurrentRow ( node ) { return ! ! ( node . parentElement . parentElement == node . parentElement . parentElement . parentElement . lastChild ) ; } youTubeAutoSubsMuteTimeout ( instance ) { const video = window . document . querySelector ( WebAudio . defaultVideoSelector ) ; if ( video && instance . playing ( video ) ) { instance . unmute ( instance . youTubeAutoSubsRule ) ; } instance . youTubeAutoSubsTimeout = null ; } youTubeAutoSubsNodeIsSubtitleText ( node ) { const captionWindow = document . querySelector ( 'div.caption-window' ) ; return ! ! ( captionWindow && captionWindow . contains ( node ) ) ; } youTubeAutoSubsPresent ( ) { return ! ! ( document . querySelector ( 'div.ytp-caption-window-rollup' ) ) ; } youTubeAutoSubsSupportedNode ( node ) { if ( node . nodeName == '#text' && node . textContent != '' ) { return ! ! ( this . youTubeAutoSubsNodeIsSubtitleText ( node ) ) ; } return false ; } }