2022-03-31 17:33:10 +02:00
// ==UserScript==
// @name TwitchAdSolutions (vaft)
// @namespace https://github.com/pixeltris/TwitchAdSolutions
2022-08-27 16:32:21 +02:00
// @version 5.4.0-f1
2022-03-31 17:33:10 +02:00
// @description Multiple solutions for blocking Twitch ads (vaft)
// @updateURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/vaft/vaft.user.js
// @downloadURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/vaft/vaft.user.js
// @author https://github.com/cleanlock/VideoAdBlockForTwitch#credits
// @match *://*.twitch.tv/*
// @run-at document-start
// @grant none
// ==/UserScript==
// This code is directly copied from https://github.com/cleanlock/VideoAdBlockForTwitch (only change is whitespace is removed for the ublock origin script - also indented)
( function ( ) {
'use strict' ;
//This stops Twitch from pausing the player when in another tab and an ad shows.
try {
Object . defineProperty ( document , 'visibilityState' , {
get ( ) {
return 'visible' ;
}
} ) ;
Object . defineProperty ( document , 'hidden' , {
get ( ) {
return false ;
}
} ) ;
const block = e => {
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
e . stopImmediatePropagation ( ) ;
} ;
const process = e => {
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
e . stopImmediatePropagation ( ) ;
//This corrects the background tab buffer bug when switching to the background tab for the first time after an extended period.
doTwitchPlayerTask ( false , false , true , false , false ) ;
} ;
document . addEventListener ( 'visibilitychange' , process , true ) ;
document . addEventListener ( 'webkitvisibilitychange' , block , true ) ;
document . addEventListener ( 'mozvisibilitychange' , block , true ) ;
document . addEventListener ( 'hasFocus' , block , true ) ;
if ( /Firefox/ . test ( navigator . userAgent ) ) {
Object . defineProperty ( document , 'mozHidden' , {
get ( ) {
return false ;
}
} ) ;
} else {
Object . defineProperty ( document , 'webkitHidden' , {
get ( ) {
return false ;
}
} ) ;
}
} catch ( err ) { }
//Send settings updates to worker.
window . addEventListener ( "message" , ( event ) => {
if ( event . source != window )
return ;
if ( event . data . type && ( event . data . type == "SetHideBlockingMessage" ) ) {
if ( twitchMainWorker ) {
twitchMainWorker . postMessage ( {
key : 'SetHideBlockingMessage' ,
value : event . data . value
} ) ;
}
}
} , false ) ;
function declareOptions ( scope ) {
scope . AdSignifier = 'stitched' ;
scope . ClientID = 'kimne78kx3ncx6brgo4mv6wki5h1ko' ;
scope . ClientVersion = 'null' ;
scope . ClientSession = 'null' ;
2022-08-27 16:32:21 +02:00
//scope.PlayerType1 = 'site'; //Source - NOTE: This is unused as it's implicitly used by the website iself
scope . PlayerType2 = 'embed' ; //Source
scope . PlayerType3 = 'proxy' ; //Source
scope . PlayerType4 = 'thunderdome' ; //480p
2022-03-31 17:33:10 +02:00
scope . CurrentChannelName = null ;
scope . UsherParams = null ;
scope . WasShowingAd = false ;
scope . GQLDeviceID = null ;
scope . HideBlockingMessage = false ;
scope . IsSquadStream = false ;
2022-08-27 16:32:21 +02:00
scope . StreamInfos = [ ] ;
scope . StreamInfosByUrl = [ ] ;
scope . MainUrlByUrl = [ ] ;
scope . EncodingCacheTimeout = 60000 ;
2022-03-31 17:33:10 +02:00
}
declareOptions ( window ) ;
var twitchMainWorker = null ;
var adBlockDiv = null ;
var OriginalVideoPlayerQuality = null ;
var IsPlayerAutoQuality = null ;
const oldWorker = window . Worker ;
window . Worker = class Worker extends oldWorker {
constructor ( twitchBlobUrl ) {
if ( twitchMainWorker ) {
super ( twitchBlobUrl ) ;
return ;
}
var jsURL = getWasmWorkerUrl ( twitchBlobUrl ) ;
if ( typeof jsURL !== 'string' ) {
super ( twitchBlobUrl ) ;
return ;
}
var newBlobStr = `
2022-08-27 16:32:21 +02:00
$ { getStreamUrlForResolution . toString ( ) }
$ { getStreamForResolution . toString ( ) }
$ { stripUnusedParams . toString ( ) }
2022-03-31 17:33:10 +02:00
$ { processM3U8 . toString ( ) }
$ { hookWorkerFetch . toString ( ) }
$ { declareOptions . toString ( ) }
$ { getAccessToken . toString ( ) }
$ { gqlRequest . toString ( ) }
$ { adRecordgqlPacket . toString ( ) }
$ { tryNotifyTwitch . toString ( ) }
$ { parseAttributes . toString ( ) }
declareOptions ( self ) ;
self . addEventListener ( 'message' , function ( e ) {
if ( e . data . key == 'UpdateIsSquadStream' ) {
IsSquadStream = e . data . value ;
} else if ( e . data . key == 'UpdateClientVersion' ) {
ClientVersion = e . data . value ;
} else if ( e . data . key == 'UpdateClientSession' ) {
ClientSession = e . data . value ;
} else if ( e . data . key == 'UpdateClientId' ) {
ClientID = e . data . value ;
} else if ( e . data . key == 'UpdateDeviceId' ) {
GQLDeviceID = e . data . value ;
} else if ( e . data . key == 'SetHideBlockingMessage' ) {
if ( e . data . value == "true" ) {
HideBlockingMessage = false ;
2022-04-04 13:01:50 +02:00
} else if ( e . data . value == "false" ) {
HideBlockingMessage = true ;
2022-03-31 17:33:10 +02:00
}
}
} ) ;
hookWorkerFetch ( ) ;
importScripts ( '${jsURL}' ) ;
` ;
super ( URL . createObjectURL ( new Blob ( [ newBlobStr ] ) ) ) ;
twitchMainWorker = this ;
this . onmessage = function ( e ) {
if ( e . data . key == 'ShowAdBlockBanner' ) {
if ( adBlockDiv == null ) {
adBlockDiv = getAdBlockDiv ( ) ;
}
adBlockDiv . P . textContent = 'Blocking ads...' ;
adBlockDiv . style . display = 'block' ;
} else if ( e . data . key == 'HideAdBlockBanner' ) {
if ( adBlockDiv == null ) {
adBlockDiv = getAdBlockDiv ( ) ;
}
adBlockDiv . style . display = 'none' ;
} else if ( e . data . key == 'PauseResumePlayer' ) {
doTwitchPlayerTask ( true , false , false , false , false ) ;
} else if ( e . data . key == 'ForceChangeQuality' ) {
//This is used to fix the bug where the video would freeze.
try {
2022-08-27 16:32:21 +02:00
if ( navigator . userAgent . toLowerCase ( ) . indexOf ( 'firefox' ) == - 1 ) {
return ;
}
2022-03-31 17:33:10 +02:00
var autoQuality = doTwitchPlayerTask ( false , false , false , true , false ) ;
var currentQuality = doTwitchPlayerTask ( false , true , false , false , false ) ;
if ( IsPlayerAutoQuality == null ) {
IsPlayerAutoQuality = autoQuality ;
}
if ( OriginalVideoPlayerQuality == null ) {
OriginalVideoPlayerQuality = currentQuality ;
}
if ( ! currentQuality . includes ( '480' ) || e . data . value != null ) {
if ( ! OriginalVideoPlayerQuality . includes ( '480' ) ) {
var settingsMenu = document . querySelector ( 'div[data-a-target="player-settings-menu"]' ) ;
if ( settingsMenu == null ) {
var settingsCog = document . querySelector ( 'button[data-a-target="player-settings-button"]' ) ;
if ( settingsCog ) {
settingsCog . click ( ) ;
var qualityMenu = document . querySelector ( 'button[data-a-target="player-settings-menu-item-quality"]' ) ;
if ( qualityMenu ) {
qualityMenu . click ( ) ;
}
var lowQuality = document . querySelectorAll ( 'input[data-a-target="tw-radio"' ) ;
if ( lowQuality ) {
var qualityToSelect = lowQuality . length - 3 ;
if ( e . data . value != null ) {
if ( e . data . value . includes ( 'original' ) ) {
e . data . value = OriginalVideoPlayerQuality ;
if ( IsPlayerAutoQuality ) {
e . data . value = 'auto' ;
}
}
if ( e . data . value . includes ( '160p' ) ) {
qualityToSelect = 5 ;
}
if ( e . data . value . includes ( '360p' ) ) {
qualityToSelect = 4 ;
}
if ( e . data . value . includes ( '480p' ) ) {
qualityToSelect = 3 ;
}
if ( e . data . value . includes ( '720p' ) ) {
qualityToSelect = 2 ;
}
if ( e . data . value . includes ( '822p' ) ) {
qualityToSelect = 2 ;
}
if ( e . data . value . includes ( '864p' ) ) {
qualityToSelect = 2 ;
}
if ( e . data . value . includes ( '900p' ) ) {
qualityToSelect = 2 ;
}
if ( e . data . value . includes ( '936p' ) ) {
qualityToSelect = 2 ;
}
if ( e . data . value . includes ( '960p' ) ) {
qualityToSelect = 2 ;
}
if ( e . data . value . includes ( '1080p' ) ) {
qualityToSelect = 2 ;
}
if ( e . data . value . includes ( 'source' ) ) {
qualityToSelect = 1 ;
}
if ( e . data . value . includes ( 'auto' ) ) {
qualityToSelect = 0 ;
}
}
2022-04-04 13:01:50 +02:00
var currentQualityLS = window . localStorage . getItem ( 'video-quality' ) ;
2022-03-31 17:33:10 +02:00
lowQuality [ qualityToSelect ] . click ( ) ;
2022-04-04 13:01:50 +02:00
window . localStorage . setItem ( 'video-quality' , currentQualityLS ) ;
2022-03-31 17:33:10 +02:00
if ( e . data . value != null ) {
OriginalVideoPlayerQuality = null ;
IsPlayerAutoQuality = null ;
doTwitchPlayerTask ( false , false , false , true , true ) ;
}
}
}
}
}
}
} catch ( err ) {
OriginalVideoPlayerQuality = null ;
IsPlayerAutoQuality = null ;
}
}
} ;
function getAdBlockDiv ( ) {
//To display a notification to the user, that an ad is being blocked.
var playerRootDiv = document . querySelector ( '.video-player' ) ;
var adBlockDiv = null ;
if ( playerRootDiv != null ) {
adBlockDiv = playerRootDiv . querySelector ( '.adblock-overlay' ) ;
if ( adBlockDiv == null ) {
adBlockDiv = document . createElement ( 'div' ) ;
adBlockDiv . className = 'adblock-overlay' ;
adBlockDiv . innerHTML = '<div class="player-adblock-notice" style="color: white; background-color: rgba(0, 0, 0, 0.8); position: absolute; top: 0px; left: 0px; padding: 5px;"><p></p></div>' ;
adBlockDiv . style . display = 'none' ;
adBlockDiv . P = adBlockDiv . querySelector ( 'p' ) ;
playerRootDiv . appendChild ( adBlockDiv ) ;
}
}
return adBlockDiv ;
}
}
} ;
function getWasmWorkerUrl ( twitchBlobUrl ) {
var req = new XMLHttpRequest ( ) ;
req . open ( 'GET' , twitchBlobUrl , false ) ;
req . send ( ) ;
return req . responseText . split ( "'" ) [ 1 ] ;
}
function hookWorkerFetch ( ) {
2022-08-27 16:32:21 +02:00
console . log ( 'Twitch adblocker is enabled' ) ;
2022-03-31 17:33:10 +02:00
var realFetch = fetch ;
fetch = async function ( url , options ) {
if ( typeof url === 'string' ) {
if ( url . includes ( 'video-weaver' ) ) {
return new Promise ( function ( resolve , reject ) {
var processAfter = async function ( response ) {
//Here we check the m3u8 for any ads and also try fallback player types if needed.
var responseText = await response . text ( ) ;
var weaverText = null ;
weaverText = await processM3U8 ( url , responseText , realFetch , PlayerType2 ) ;
if ( weaverText . includes ( AdSignifier ) ) {
weaverText = await processM3U8 ( url , responseText , realFetch , PlayerType3 ) ;
}
if ( weaverText . includes ( AdSignifier ) ) {
weaverText = await processM3U8 ( url , responseText , realFetch , PlayerType4 ) ;
}
resolve ( new Response ( weaverText ) ) ;
} ;
var send = function ( ) {
return realFetch ( url , options ) . then ( function ( response ) {
processAfter ( response ) ;
} ) [ 'catch' ] ( function ( err ) {
reject ( err ) ;
} ) ;
} ;
send ( ) ;
} ) ;
} else if ( url . includes ( '/api/channel/hls/' ) ) {
var channelName = ( new URL ( url ) ) . pathname . match ( /([^\/]+)(?=\.\w+$)/ ) [ 0 ] ;
UsherParams = ( new URL ( url ) ) . search ;
CurrentChannelName = channelName ;
//To prevent pause/resume loop for mid-rolls.
var isPBYPRequest = url . includes ( 'picture-by-picture' ) ;
if ( isPBYPRequest ) {
url = '' ;
}
2022-08-27 16:32:21 +02:00
return new Promise ( function ( resolve , reject ) {
var processAfter = async function ( response ) {
encodingsM3u8 = await response . text ( ) ;
var streamInfo = StreamInfos [ channelName ] ;
if ( streamInfo == null ) {
StreamInfos [ channelName ] = streamInfo = { } ;
}
streamInfo . ChannelName = channelName ;
streamInfo . Urls = [ ] ; // xxx.m3u8 -> "284x160" (resolution)
streamInfo . EncodingsM3U8Cache = [ ] ;
var lines = encodingsM3u8 . replace ( '\r' , '' ) . split ( '\n' ) ;
for ( var i = 0 ; i < lines . length ; i ++ ) {
if ( ! lines [ i ] . startsWith ( '#' ) && lines [ i ] . includes ( '.m3u8' ) ) {
streamInfo . Urls [ lines [ i ] ] = - 1 ;
if ( i > 0 && lines [ i - 1 ] . startsWith ( '#EXT-X-STREAM-INF' ) ) {
var res = parseAttributes ( lines [ i - 1 ] ) [ 'RESOLUTION' ] ;
if ( res ) {
streamInfo . Urls [ lines [ i ] ] = res ;
}
}
StreamInfosByUrl [ lines [ i ] ] = streamInfo ;
MainUrlByUrl [ lines [ i ] ] = url ;
2022-03-31 17:33:10 +02:00
}
2022-08-27 16:32:21 +02:00
}
resolve ( new Response ( encodingsM3u8 ) ) ;
} ;
var send = function ( ) {
return realFetch ( url , options ) . then ( function ( response ) {
processAfter ( response ) ;
} ) [ 'catch' ] ( function ( err ) {
reject ( err ) ;
} ) ;
} ;
send ( ) ;
} ) ;
2022-03-31 17:33:10 +02:00
}
}
return realFetch . apply ( this , arguments ) ;
} ;
}
2022-08-27 16:32:21 +02:00
function getStreamUrlForResolution ( targetResolution , encodingsM3u8 ) {
var encodingsLines = encodingsM3u8 . replace ( '\r' , '' ) . split ( '\n' ) ;
var firstUrl = null ;
for ( var i = 0 ; i < encodingsLines . length ; i ++ ) {
if ( ! encodingsLines [ i ] . startsWith ( '#' ) && encodingsLines [ i ] . includes ( '.m3u8' ) ) {
if ( i > 0 && encodingsLines [ i - 1 ] . startsWith ( '#EXT-X-STREAM-INF' ) ) {
var res = parseAttributes ( encodingsLines [ i - 1 ] ) [ 'RESOLUTION' ] ;
if ( res && ( ! targetResolution || res == targetResolution ) ) {
return encodingsLines [ i ] ;
}
if ( firstUrl == null ) {
firstUrl = encodingsLines [ i ] ;
}
2022-03-31 17:33:10 +02:00
}
2022-08-27 16:32:21 +02:00
}
}
return firstUrl ;
}
async function getStreamForResolution ( streamInfo , targetResolution , encodingsM3u8 , fallbackStreamStr , playerType , realFetch ) {
if ( streamInfo . EncodingsM3U8Cache [ playerType ] . Resolution != targetResolution ||
streamInfo . EncodingsM3U8Cache [ playerType ] . RequestTime < Date . now ( ) - EncodingCacheTimeout ) {
console . log ( ` Blocking ads (type: ${ playerType } , resolution: ${ targetResolution } ) ` ) ;
}
streamInfo . EncodingsM3U8Cache [ playerType ] . RequestTime = Date . now ( ) ;
streamInfo . EncodingsM3U8Cache [ playerType ] . Value = encodingsM3u8 ;
streamInfo . EncodingsM3U8Cache [ playerType ] . Resolution = targetResolution ;
var streamM3u8Url = getStreamUrlForResolution ( targetResolution , encodingsM3u8 ) ;
var streamM3u8Response = await realFetch ( streamM3u8Url ) ;
if ( streamM3u8Response . status == 200 ) {
var m3u8Text = await streamM3u8Response . text ( ) ;
WasShowingAd = true ;
if ( HideBlockingMessage == false ) {
postMessage ( {
key : 'ShowAdBlockBanner'
} ) ;
} else if ( HideBlockingMessage == true ) {
postMessage ( {
key : 'HideAdBlockBanner'
} ) ;
}
postMessage ( {
key : 'ForceChangeQuality'
} ) ;
if ( ! m3u8Text || m3u8Text . includes ( AdSignifier ) ) {
streamInfo . EncodingsM3U8Cache [ playerType ] . Value = null ;
}
return m3u8Text ;
2022-03-31 17:33:10 +02:00
} else {
2022-08-27 16:32:21 +02:00
streamInfo . EncodingsM3U8Cache [ playerType ] . Value = null ;
return fallbackStreamStr ;
2022-03-31 17:33:10 +02:00
}
}
2022-08-27 16:32:21 +02:00
function stripUnusedParams ( str , params ) {
if ( ! params ) {
params = [ 'token' , 'sig' ] ;
}
var tempUrl = new URL ( 'https://localhost/' + str ) ;
for ( var i = 0 ; i < params . length ; i ++ ) {
tempUrl . searchParams . delete ( params [ i ] ) ;
}
return tempUrl . pathname . substring ( 1 ) + tempUrl . search ;
}
2022-03-31 17:33:10 +02:00
async function processM3U8 ( url , textStr , realFetch , playerType ) {
//Checks the m3u8 for ads and if it finds one, instead returns an ad-free stream.
2022-08-27 16:32:21 +02:00
var streamInfo = StreamInfosByUrl [ url ] ;
2022-03-31 17:33:10 +02:00
//Ad blocking for squad streams is disabled due to the way multiple weaver urls are used. No workaround so far.
if ( IsSquadStream == true ) {
return textStr ;
}
if ( ! textStr ) {
return textStr ;
}
//Some live streams use mp4.
if ( ! textStr . includes ( ".ts" ) && ! textStr . includes ( ".mp4" ) ) {
return textStr ;
}
var haveAdTags = textStr . includes ( AdSignifier ) ;
if ( haveAdTags ) {
2022-08-27 16:32:21 +02:00
var isMidroll = textStr . includes ( '"MIDROLL"' ) || textStr . includes ( '"midroll"' ) ;
//Reduces ad frequency. TODO: Reduce the number of requests. This is really spamming Twitch with requests.
if ( ! isMidroll ) {
try {
tryNotifyTwitch ( textStr ) ;
} catch ( err ) { }
}
var currentResolution = null ;
if ( streamInfo && streamInfo . Urls ) {
for ( const [ resUrl , resName ] of Object . entries ( streamInfo . Urls ) ) {
if ( resUrl == url ) {
currentResolution = resName ;
//console.log(resName);
break ;
}
}
}
// Keep the m3u8 around for a little while (once per ad) before requesting a new one
var encodingsM3U8Cache = streamInfo . EncodingsM3U8Cache [ playerType ] ;
if ( encodingsM3U8Cache ) {
if ( encodingsM3U8Cache . Value && encodingsM3U8Cache . RequestTime >= Date . now ( ) - EncodingCacheTimeout ) {
try {
var result = getStreamForResolution ( streamInfo , currentResolution , encodingsM3U8Cache . Value , null , playerType , realFetch ) ;
if ( result ) {
return result ;
}
} catch ( err ) {
encodingsM3U8Cache . Value = null ;
}
}
} else {
streamInfo . EncodingsM3U8Cache [ playerType ] = {
RequestTime : Date . now ( ) ,
Value : null ,
Resolution : null
} ;
}
if ( playerType === 'proxy' ) {
try {
/ * v a r t e m p U r l = s t r i p U n u s e d P a r a m s ( M a i n U r l B y U r l [ u r l ] ) ;
const match = /(hls|vod)\/(.+?)$/gim . exec ( tempUrl ) ; * /
var encodingsM3u8Response = await realFetch ( 'https://api.ttv.lol/playlist/' + CurrentChannelName + '.m3u8%3Fallow_source%3Dtrue' /* + encodeURIComponent(match[2])*/ , { headers : { 'X-Donate-To' : 'https://ttv.lol/donate' } } ) ;
if ( encodingsM3u8Response . status === 200 ) {
return getStreamForResolution ( streamInfo , currentResolution , await encodingsM3u8Response . text ( ) , textStr , playerType , realFetch ) ;
}
} catch ( err ) { }
return textStr ;
}
2022-03-31 17:33:10 +02:00
var accessTokenResponse = await getAccessToken ( CurrentChannelName , playerType ) ;
if ( accessTokenResponse . status === 200 ) {
var accessToken = await accessTokenResponse . json ( ) ;
try {
var urlInfo = new URL ( 'https://usher.ttvnw.net/api/channel/hls/' + CurrentChannelName + '.m3u8' + UsherParams ) ;
urlInfo . searchParams . set ( 'sig' , accessToken . data . streamPlaybackAccessToken . signature ) ;
urlInfo . searchParams . set ( 'token' , accessToken . data . streamPlaybackAccessToken . value ) ;
var encodingsM3u8Response = await realFetch ( urlInfo . href ) ;
if ( encodingsM3u8Response . status === 200 ) {
2022-08-27 16:32:21 +02:00
return getStreamForResolution ( streamInfo , currentResolution , await encodingsM3u8Response . text ( ) , textStr , playerType , realFetch ) ;
2022-03-31 17:33:10 +02:00
} else {
return textStr ;
}
} catch ( err ) { }
return textStr ;
} else {
return textStr ;
}
} else {
if ( WasShowingAd ) {
2022-04-04 13:01:50 +02:00
console . log ( "Done blocking ads, changing back to original quality" ) ;
2022-03-31 17:33:10 +02:00
WasShowingAd = false ;
//Here we put player back to original quality and remove the blocking message.
postMessage ( {
key : 'ForceChangeQuality' ,
value : 'original'
} ) ;
postMessage ( {
key : 'PauseResumePlayer'
} ) ;
postMessage ( {
key : 'HideAdBlockBanner'
} ) ;
}
return textStr ;
}
return textStr ;
}
function parseAttributes ( str ) {
return Object . fromEntries (
str . split ( /(?:^|,)((?:[^=]*)=(?:"[^"]*"|[^,]*))/ )
. filter ( Boolean )
. map ( x => {
const idx = x . indexOf ( '=' ) ;
const key = x . substring ( 0 , idx ) ;
const value = x . substring ( idx + 1 ) ;
const num = Number ( value ) ;
return [ key , Number . isNaN ( num ) ? value . startsWith ( '"' ) ? JSON . parse ( value ) : value : num ] ;
} ) ) ;
}
async function tryNotifyTwitch ( streamM3u8 ) {
//We notify that an ad was requested but was not visible and was also muted.
var matches = streamM3u8 . match ( /#EXT-X-DATERANGE:(ID="stitched-ad-[^\n]+)\n/ ) ;
if ( matches . length > 1 ) {
const attrString = matches [ 1 ] ;
const attr = parseAttributes ( attrString ) ;
var podLength = parseInt ( attr [ 'X-TV-TWITCH-AD-POD-LENGTH' ] ? attr [ 'X-TV-TWITCH-AD-POD-LENGTH' ] : '1' ) ;
var podPosition = parseInt ( attr [ 'X-TV-TWITCH-AD-POD-POSITION' ] ? attr [ 'X-TV-TWITCH-AD-POD-POSITION' ] : '0' ) ;
var radToken = attr [ 'X-TV-TWITCH-AD-RADS-TOKEN' ] ;
var lineItemId = attr [ 'X-TV-TWITCH-AD-LINE-ITEM-ID' ] ;
var orderId = attr [ 'X-TV-TWITCH-AD-ORDER-ID' ] ;
var creativeId = attr [ 'X-TV-TWITCH-AD-CREATIVE-ID' ] ;
var adId = attr [ 'X-TV-TWITCH-AD-ADVERTISER-ID' ] ;
var rollType = attr [ 'X-TV-TWITCH-AD-ROLL-TYPE' ] . toLowerCase ( ) ;
const baseData = {
stitched : true ,
roll _type : rollType ,
player _mute : true ,
player _volume : 0.0 ,
visible : false ,
} ;
for ( let podPosition = 0 ; podPosition < podLength ; podPosition ++ ) {
const extendedData = {
... baseData ,
ad _id : adId ,
ad _position : podPosition ,
duration : 0 ,
creative _id : creativeId ,
total _ads : podLength ,
order _id : orderId ,
line _item _id : lineItemId ,
} ;
await gqlRequest ( adRecordgqlPacket ( 'video_ad_impression' , radToken , extendedData ) ) ;
for ( let quartile = 0 ; quartile < 4 ; quartile ++ ) {
await gqlRequest (
adRecordgqlPacket ( 'video_ad_quartile_complete' , radToken , {
... extendedData ,
quartile : quartile + 1 ,
} )
) ;
}
await gqlRequest ( adRecordgqlPacket ( 'video_ad_pod_complete' , radToken , baseData ) ) ;
}
}
}
function adRecordgqlPacket ( event , radToken , payload ) {
return [ {
operationName : 'ClientSideAdEventHandling_RecordAdEvent' ,
variables : {
input : {
eventName : event ,
eventPayload : JSON . stringify ( payload ) ,
radToken ,
} ,
} ,
extensions : {
persistedQuery : {
version : 1 ,
sha256Hash : '7e6c69e6eb59f8ccb97ab73686f3d8b7d85a72a0298745ccd8bfc68e4054ca5b' ,
} ,
} ,
} ] ;
}
function getAccessToken ( channelName , playerType , realFetch ) {
var body = null ;
var templateQuery = 'query PlaybackAccessToken_Template($login: String!, $isLive: Boolean!, $vodID: ID!, $isVod: Boolean!, $playerType: String!) { streamPlaybackAccessToken(channelName: $login, params: {platform: "web", playerBackend: "mediaplayer", playerType: $playerType}) @include(if: $isLive) { value signature __typename } videoPlaybackAccessToken(id: $vodID, params: {platform: "web", playerBackend: "mediaplayer", playerType: $playerType}) @include(if: $isVod) { value signature __typename }}' ;
body = {
operationName : 'PlaybackAccessToken_Template' ,
query : templateQuery ,
variables : {
'isLive' : true ,
'login' : channelName ,
'isVod' : false ,
'vodID' : '' ,
'playerType' : playerType
}
} ;
return gqlRequest ( body , realFetch ) ;
}
function gqlRequest ( body , realFetch ) {
var fetchFunc = realFetch ? realFetch : fetch ;
if ( ! GQLDeviceID ) {
var dcharacters = 'abcdefghijklmnopqrstuvwxyz0123456789' ;
var dcharactersLength = dcharacters . length ;
for ( var i = 0 ; i < 32 ; i ++ ) {
GQLDeviceID += dcharacters . charAt ( Math . floor ( Math . random ( ) * dcharactersLength ) ) ;
}
}
return fetchFunc ( 'https://gql.twitch.tv/gql' , {
method : 'POST' ,
body : JSON . stringify ( body ) ,
headers : {
'Client-ID' : ClientID ,
'Device-ID' : GQLDeviceID ,
'X-Device-Id' : GQLDeviceID ,
'Client-Version' : ClientVersion ,
'Client-Session-Id' : ClientSession
}
} ) ;
}
function doTwitchPlayerTask ( isPausePlay , isCheckQuality , isCorrectBuffer , isAutoQuality , setAutoQuality ) {
//This will do an instant pause/play to return to original quality once the ad is finished.
//We also use this function to get the current video player quality set by the user.
//We also use this function to quickly pause/play the player when switching tabs to stop delays.
try {
var videoController = null ;
var videoPlayer = null ;
function findReactNode ( root , constraint ) {
if ( root . stateNode && constraint ( root . stateNode ) ) {
return root . stateNode ;
}
let node = root . child ;
while ( node ) {
const result = findReactNode ( node , constraint ) ;
if ( result ) {
return result ;
}
node = node . sibling ;
}
return null ;
}
var reactRootNode = null ;
var rootNode = document . querySelector ( '#root' ) ;
if ( rootNode && rootNode . _reactRootContainer && rootNode . _reactRootContainer . _internalRoot && rootNode . _reactRootContainer . _internalRoot . current ) {
reactRootNode = rootNode . _reactRootContainer . _internalRoot . current ;
}
videoPlayer = findReactNode ( reactRootNode , node => node . setPlayerActive && node . props && node . props . mediaPlayerInstance ) ;
videoPlayer = videoPlayer && videoPlayer . props && videoPlayer . props . mediaPlayerInstance ? videoPlayer . props . mediaPlayerInstance : null ;
if ( isPausePlay ) {
videoPlayer . pause ( ) ;
videoPlayer . play ( ) ;
return ;
}
if ( isCheckQuality ) {
if ( typeof videoPlayer . getQuality ( ) == 'undefined' ) {
return ;
}
var playerQuality = JSON . stringify ( videoPlayer . getQuality ( ) ) ;
if ( playerQuality ) {
return playerQuality ;
} else {
return ;
}
}
if ( isAutoQuality ) {
if ( typeof videoPlayer . isAutoQualityMode ( ) == 'undefined' ) {
return false ;
}
var autoQuality = videoPlayer . isAutoQualityMode ( ) ;
if ( autoQuality ) {
videoPlayer . setAutoQualityMode ( false ) ;
return autoQuality ;
} else {
return false ;
}
}
if ( setAutoQuality ) {
videoPlayer . setAutoQualityMode ( true ) ;
return ;
}
//This only happens when switching tabs and is to correct the high latency caused when opening background tabs and going to them at a later time.
//We check that this is a live stream by the page URL, to prevent vod/clip pause/plays.
try {
var currentPageURL = document . URL ;
var isLive = true ;
if ( currentPageURL . includes ( 'videos/' ) || currentPageURL . includes ( 'clip/' ) ) {
isLive = false ;
}
if ( isCorrectBuffer && isLive ) {
//A timer is needed due to the player not resuming without it.
setTimeout ( function ( ) {
//If latency to broadcaster is above 5 or 15 seconds upon switching tabs, we pause and play the player to reset the latency.
//If latency is between 0-6, user can manually pause and resume to reset latency further.
if ( videoPlayer . isLiveLowLatency ( ) && videoPlayer . getLiveLatency ( ) > 5 ) {
videoPlayer . pause ( ) ;
videoPlayer . play ( ) ;
} else if ( videoPlayer . getLiveLatency ( ) > 15 ) {
videoPlayer . pause ( ) ;
videoPlayer . play ( ) ;
}
} , 3000 ) ;
}
} catch ( err ) { }
} catch ( err ) { }
}
var localDeviceID = null ;
localDeviceID = window . localStorage . getItem ( 'local_copy_unique_id' ) ;
function hookFetch ( ) {
var realFetch = window . fetch ;
window . fetch = function ( url , init , ... args ) {
if ( typeof url === 'string' ) {
//Check if squad stream.
if ( window . location . pathname . includes ( '/squad' ) ) {
if ( twitchMainWorker ) {
twitchMainWorker . postMessage ( {
key : 'UpdateIsSquadStream' ,
value : true
} ) ;
}
} else {
if ( twitchMainWorker ) {
twitchMainWorker . postMessage ( {
key : 'UpdateIsSquadStream' ,
value : false
} ) ;
}
}
if ( url . includes ( '/access_token' ) || url . includes ( 'gql' ) ) {
//Device ID is used when notifying Twitch of ads.
var deviceId = init . headers [ 'X-Device-Id' ] ;
if ( typeof deviceId !== 'string' ) {
deviceId = init . headers [ 'Device-ID' ] ;
}
//Added to prevent eventual UBlock conflicts.
if ( typeof deviceId === 'string' && ! deviceId . includes ( 'twitch-web-wall-mason' ) ) {
GQLDeviceID = deviceId ;
} else if ( localDeviceID ) {
GQLDeviceID = localDeviceID . replace ( '"' , '' ) ;
GQLDeviceID = GQLDeviceID . replace ( '"' , '' ) ;
}
if ( GQLDeviceID && twitchMainWorker ) {
if ( typeof init . headers [ 'X-Device-Id' ] === 'string' ) {
init . headers [ 'X-Device-Id' ] = GQLDeviceID ;
}
if ( typeof init . headers [ 'Device-ID' ] === 'string' ) {
init . headers [ 'Device-ID' ] = GQLDeviceID ;
}
twitchMainWorker . postMessage ( {
key : 'UpdateDeviceId' ,
value : GQLDeviceID
} ) ;
}
//Client version is used in GQL requests.
var clientVersion = init . headers [ 'Client-Version' ] ;
if ( clientVersion && typeof clientVersion == 'string' ) {
ClientVersion = clientVersion ;
}
if ( ClientVersion && twitchMainWorker ) {
twitchMainWorker . postMessage ( {
key : 'UpdateClientVersion' ,
value : ClientVersion
} ) ;
}
//Client session is used in GQL requests.
var clientSession = init . headers [ 'Client-Session-Id' ] ;
if ( clientSession && typeof clientSession == 'string' ) {
ClientSession = clientSession ;
}
if ( ClientSession && twitchMainWorker ) {
twitchMainWorker . postMessage ( {
key : 'UpdateClientSession' ,
value : ClientSession
} ) ;
}
//Client ID is used in GQL requests.
if ( url . includes ( 'gql' ) && init && typeof init . body === 'string' && init . body . includes ( 'PlaybackAccessToken' ) ) {
var clientId = init . headers [ 'Client-ID' ] ;
if ( clientId && typeof clientId == 'string' ) {
ClientID = clientId ;
} else {
clientId = init . headers [ 'Client-Id' ] ;
if ( clientId && typeof clientId == 'string' ) {
ClientID = clientId ;
}
}
if ( ClientID && twitchMainWorker ) {
twitchMainWorker . postMessage ( {
key : 'UpdateClientId' ,
value : ClientID
} ) ;
}
}
//To prevent pause/resume loop for mid-rolls.
if ( url . includes ( 'gql' ) && init && typeof init . body === 'string' && init . body . includes ( 'PlaybackAccessToken' ) && init . body . includes ( 'picture-by-picture' ) ) {
init . body = '' ;
}
var isPBYPRequest = url . includes ( 'picture-by-picture' ) ;
if ( isPBYPRequest ) {
url = '' ;
}
}
}
return realFetch . apply ( this , arguments ) ;
} ;
}
hookFetch ( ) ;
2022-04-04 13:01:50 +02:00
} ) ( ) ;