2022-12-28 17:52:48 +01:00
twitch - videoad . js text / javascript
2022-11-19 21:47:54 +01:00
( function ( ) {
if ( /(^|\.)twitch\.tv$/ . test ( document . location . hostname ) === false ) { return ; }
function declareOptions ( scope ) {
// Options / globals
2023-06-01 09:03:45 +02:00
scope . OPT _ROLLING _DEVICE _ID = false ;
2022-11-19 21:47:54 +01:00
scope . OPT _MODE _STRIP _AD _SEGMENTS = true ;
scope . OPT _MODE _NOTIFY _ADS _WATCHED = true ;
scope . OPT _MODE _NOTIFY _ADS _WATCHED _MIN _REQUESTS = false ;
2023-06-01 10:21:49 +02:00
scope . OPT _BACKUP _PLAYER _TYPE = 'autoplay' ;
scope . OPT _BACKUP _PLATFORM = 'ios' ;
2022-11-19 21:47:54 +01:00
scope . OPT _REGULAR _PLAYER _TYPE = 'site' ;
2023-06-01 09:03:45 +02:00
scope . OPT _ACCESS _TOKEN _PLAYER _TYPE = null ;
2023-06-01 10:21:49 +02:00
scope . OPT _SHOW _AD _BANNER = true ;
2022-11-19 21:47:54 +01:00
scope . AD _SIGNIFIER = 'stitched-ad' ;
scope . LIVE _SIGNIFIER = ',live' ;
scope . CLIENT _ID = 'kimne78kx3ncx6brgo4mv6wki5h1ko' ;
// These are only really for Worker scope...
scope . StreamInfos = [ ] ;
scope . StreamInfosByUrl = [ ] ;
scope . CurrentChannelNameFromM3U8 = null ;
// Need this in both scopes. Window scope needs to update this to worker scope.
scope . gql _device _id = null ;
scope . gql _device _id _rolling = '' ;
// Rolling device id crap... TODO: improve this
var charTable = [ ] ; for ( var i = 97 ; i <= 122 ; i ++ ) { charTable . push ( String . fromCharCode ( i ) ) ; } for ( var i = 65 ; i <= 90 ; i ++ ) { charTable . push ( String . fromCharCode ( i ) ) ; } for ( var i = 48 ; i <= 57 ; i ++ ) { charTable . push ( String . fromCharCode ( i ) ) ; }
var bs = 'eVI6jx47kJvCFfFowK86eVI6jx47kJvC' ;
var di = ( new Date ( ) ) . getUTCFullYear ( ) + ( new Date ( ) ) . getUTCMonth ( ) + ( ( new Date ( ) ) . getUTCDate ( ) / 7 ) | 0 ;
for ( var i = 0 ; i < bs . length ; i ++ ) {
scope . gql _device _id _rolling += charTable [ ( bs . charCodeAt ( i ) ^ di ) % charTable . length ] ;
}
scope . gql _device _id _rolling = '1' ; //temporary
2023-06-01 11:33:30 +02:00
scope . ClientIntegrityHeader = null ;
scope . AuthorizationHeader = null ;
2022-11-19 21:47:54 +01:00
}
declareOptions ( window ) ;
var twitchMainWorker = 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 = `
$ { processM3U8 . toString ( ) }
$ { hookWorkerFetch . toString ( ) }
$ { declareOptions . toString ( ) }
$ { getAccessToken . toString ( ) }
$ { gqlRequest . toString ( ) }
$ { makeGraphQlPacket . toString ( ) }
$ { tryNotifyAdsWatchedM3U8 . toString ( ) }
$ { parseAttributes . toString ( ) }
$ { onFoundAd . toString ( ) }
declareOptions ( self ) ;
self . addEventListener ( 'message' , function ( e ) {
if ( e . data . key == 'UboUpdateDeviceId' ) {
gql _device _id = e . data . value ;
2023-06-01 11:33:30 +02:00
} else if ( e . data . key == 'UpdateClientIntegrityHeader' ) {
ClientIntegrityHeader = e . data . value ;
} else if ( e . data . key == 'UpdateAuthorizationHeader' ) {
AuthorizationHeader = e . data . value ;
2022-11-19 21:47:54 +01:00
}
} ) ;
hookWorkerFetch ( ) ;
importScripts ( '${jsURL}' ) ;
`
super ( URL . createObjectURL ( new Blob ( [ newBlobStr ] ) ) ) ;
twitchMainWorker = this ;
this . onmessage = function ( e ) {
// NOTE: Removed adDiv caching as '.video-player' can change between streams?
if ( e . data . key == 'UboShowAdBanner' ) {
var adDiv = getAdDiv ( ) ;
if ( adDiv != null ) {
2023-06-01 11:33:30 +02:00
adDiv . P . textContent = 'Blocking' + ( e . data . isMidroll ? ' midroll' : '' ) + ' ads' ;
2023-06-01 10:21:49 +02:00
if ( OPT _SHOW _AD _BANNER ) {
adDiv . style . display = 'block' ;
}
2022-11-19 21:47:54 +01:00
}
2023-06-01 09:03:45 +02:00
} else if ( e . data . key == 'UboHideAdBanner' ) {
2022-11-19 21:47:54 +01:00
var adDiv = getAdDiv ( ) ;
if ( adDiv != null ) {
adDiv . style . display = 'none' ;
}
2023-06-01 09:03:45 +02:00
} else if ( e . data . key == 'UboChannelNameM3U8Changed' ) {
2022-11-19 21:47:54 +01:00
//console.log('M3U8 channel name changed to ' + e.data.value);
2023-06-01 09:03:45 +02:00
} else if ( e . data . key == 'UboReloadPlayer' ) {
2022-11-19 21:47:54 +01:00
reloadTwitchPlayer ( ) ;
2023-06-01 09:03:45 +02:00
} else if ( e . data . key == 'UboPauseResumePlayer' ) {
2022-11-19 21:47:54 +01:00
reloadTwitchPlayer ( false , true ) ;
2023-06-01 09:03:45 +02:00
} else if ( e . data . key == 'UboSeekPlayer' ) {
2022-11-19 21:47:54 +01:00
reloadTwitchPlayer ( true ) ;
}
}
function getAdDiv ( ) {
var playerRootDiv = document . querySelector ( '.video-player' ) ;
var adDiv = null ;
if ( playerRootDiv != null ) {
adDiv = playerRootDiv . querySelector ( '.ubo-overlay' ) ;
if ( adDiv == null ) {
adDiv = document . createElement ( 'div' ) ;
adDiv . className = 'ubo-overlay' ;
adDiv . innerHTML = '<div class="player-ad-notice" style="color: white; background-color: rgba(0, 0, 0, 0.8); position: absolute; top: 0px; left: 0px; padding: 5px;"><p></p></div>' ;
adDiv . style . display = 'none' ;
adDiv . P = adDiv . querySelector ( 'p' ) ;
playerRootDiv . appendChild ( adDiv ) ;
}
}
return adDiv ;
}
}
}
function getWasmWorkerUrl ( twitchBlobUrl ) {
var req = new XMLHttpRequest ( ) ;
req . open ( 'GET' , twitchBlobUrl , false ) ;
req . send ( ) ;
return req . responseText . split ( "'" ) [ 1 ] ;
}
function onFoundAd ( streamInfo , textStr , reloadPlayer ) {
console . log ( 'Found ads, switch to backup' ) ;
streamInfo . UseBackupStream = true ;
streamInfo . IsMidroll = textStr . includes ( '"MIDROLL"' ) || textStr . includes ( '"midroll"' ) ;
if ( reloadPlayer ) {
postMessage ( { key : 'UboReloadPlayer' } ) ;
}
postMessage ( { key : 'UboShowAdBanner' , isMidroll : streamInfo . IsMidroll } ) ;
}
async function processM3U8 ( url , textStr , realFetch ) {
var streamInfo = StreamInfosByUrl [ url ] ;
if ( streamInfo == null ) {
console . log ( 'Unknown stream url ' + url ) ;
//postMessage({key:'UboHideAdBanner'});
return textStr ;
}
if ( ! OPT _MODE _STRIP _AD _SEGMENTS ) {
return textStr ;
}
var haveAdTags = textStr . includes ( AD _SIGNIFIER ) ;
if ( streamInfo . UseBackupStream ) {
if ( streamInfo . Encodings == null ) {
console . log ( 'Found backup stream but not main stream?' ) ;
streamInfo . UseBackupStream = false ;
postMessage ( { key : 'UboReloadPlayer' } ) ;
return '' ;
} else {
var streamM3u8Url = streamInfo . Encodings . match ( /^https:.*\.m3u8$/m ) [ 0 ] ;
var streamM3u8Response = await realFetch ( streamM3u8Url ) ;
if ( streamM3u8Response . status == 200 ) {
var streamM3u8 = await streamM3u8Response . text ( ) ;
2023-10-11 12:08:30 +02:00
if ( streamM3u8 != null ) {
if ( ! streamM3u8 . includes ( AD _SIGNIFIER ) ) {
console . log ( 'No more ads on main stream. Triggering player reload to go back to main stream...' ) ;
streamInfo . UseBackupStream = false ;
postMessage ( { key : 'UboHideAdBanner' } ) ;
postMessage ( { key : 'UboReloadPlayer' } ) ;
} else if ( ! streamM3u8 . includes ( '"MIDROLL"' ) && ! streamM3u8 . includes ( '"midroll"' ) ) {
var lines = streamM3u8 . replace ( '\r' , '' ) . split ( '\n' ) ;
for ( var i = 0 ; i < lines . length ; i ++ ) {
var line = lines [ i ] ;
if ( line . startsWith ( '#EXTINF' ) && lines . length > i + 1 ) {
if ( ! line . includes ( LIVE _SIGNIFIER ) && ! streamInfo . RequestedAds . has ( lines [ i + 1 ] ) ) {
// Only request one .ts file per .m3u8 request to avoid making too many requests
//console.log('Fetch ad .ts file');
streamInfo . RequestedAds . add ( lines [ i + 1 ] ) ;
fetch ( lines [ i + 1 ] ) . then ( ( response ) => { response . blob ( ) } ) ;
break ;
}
}
}
}
2022-11-19 21:47:54 +01:00
}
}
}
if ( streamInfo . BackupEncodings == null ) {
return '' ;
}
} else if ( haveAdTags ) {
onFoundAd ( streamInfo , textStr , true ) ;
return '' ;
} else {
postMessage ( { key : 'UboHideAdBanner' } ) ;
}
return textStr ;
}
function hookWorkerFetch ( ) {
console . log ( 'hookWorkerFetch' ) ;
var realFetch = fetch ;
fetch = async function ( url , options ) {
if ( typeof url === 'string' ) {
url = url . trimEnd ( ) ;
if ( url . endsWith ( 'm3u8' ) ) {
return new Promise ( function ( resolve , reject ) {
var processAfter = async function ( response ) {
var str = await processM3U8 ( url , await response . text ( ) , realFetch ) ;
resolve ( new Response ( str ) ) ;
} ;
var send = function ( ) {
return realFetch ( url , options ) . then ( function ( response ) {
processAfter ( response ) ;
} ) [ 'catch' ] ( function ( err ) {
console . log ( 'fetch hook err ' + err ) ;
reject ( err ) ;
} ) ;
} ;
send ( ) ;
} ) ;
}
else if ( url . includes ( '/api/channel/hls/' ) && ! url . includes ( 'picture-by-picture' ) ) {
var channelName = ( new URL ( url ) ) . pathname . match ( /([^\/]+)(?=\.\w+$)/ ) [ 0 ] ;
if ( CurrentChannelNameFromM3U8 != channelName ) {
postMessage ( {
key : 'UboChannelNameM3U8Changed' ,
value : channelName
} ) ;
}
CurrentChannelNameFromM3U8 = channelName ;
if ( OPT _MODE _STRIP _AD _SEGMENTS ) {
return new Promise ( async function ( resolve , reject ) {
// - First m3u8 request is the m3u8 with the video encodings (360p,480p,720p,etc).
// - Second m3u8 request is the m3u8 for the given encoding obtained in the first request. At this point we will know if there's ads.
var streamInfo = StreamInfos [ channelName ] ;
var useBackupStream = false ;
if ( streamInfo == null || streamInfo . Encodings == null || streamInfo . BackupEncodings == null ) {
StreamInfos [ channelName ] = streamInfo = {
2023-10-11 12:08:30 +02:00
RequestedAds : new Set ( ) ,
2022-11-19 21:47:54 +01:00
Encodings : null ,
BackupEncodings : null ,
IsMidroll : false ,
UseBackupStream : false ,
ChannelName : channelName
} ;
for ( var i = 0 ; i < 2 ; i ++ ) {
var encodingsUrl = url ;
if ( i == 1 ) {
2023-06-01 10:21:49 +02:00
var accessTokenResponse = await getAccessToken ( channelName , OPT _BACKUP _PLAYER _TYPE , OPT _BACKUP _PLATFORM , realFetch ) ;
2022-11-19 21:47:54 +01:00
if ( accessTokenResponse != null && accessTokenResponse . status === 200 ) {
var accessToken = await accessTokenResponse . json ( ) ;
var urlInfo = new URL ( 'https://usher.ttvnw.net/api/channel/hls/' + channelName + '.m3u8' + ( new URL ( url ) ) . search ) ;
urlInfo . searchParams . set ( 'sig' , accessToken . data . streamPlaybackAccessToken . signature ) ;
urlInfo . searchParams . set ( 'token' , accessToken . data . streamPlaybackAccessToken . value ) ;
encodingsUrl = urlInfo . href ;
} else {
resolve ( accessTokenResponse ) ;
return ;
}
}
var encodingsM3u8Response = await realFetch ( encodingsUrl , options ) ;
if ( encodingsM3u8Response != null && encodingsM3u8Response . status === 200 ) {
var encodingsM3u8 = await encodingsM3u8Response . text ( ) ;
if ( i == 0 ) {
streamInfo . Encodings = encodingsM3u8 ;
var streamM3u8Url = encodingsM3u8 . match ( /^https:.*\.m3u8$/m ) [ 0 ] ;
var streamM3u8Response = await realFetch ( streamM3u8Url ) ;
if ( streamM3u8Response . status == 200 ) {
var streamM3u8 = await streamM3u8Response . text ( ) ;
if ( streamM3u8 . includes ( AD _SIGNIFIER ) ) {
onFoundAd ( streamInfo , streamM3u8 , false ) ;
}
} else {
resolve ( streamM3u8Response ) ;
return ;
}
} else {
2023-08-09 02:07:38 +02:00
var lowResLines = encodingsM3u8 . replace ( '\r' , '' ) . split ( '\n' ) ;
var lowResBestUrl = null ;
for ( var j = 0 ; j < lowResLines . length ; j ++ ) {
if ( lowResLines [ j ] . startsWith ( '#EXT-X-STREAM-INF' ) ) {
var res = parseAttributes ( lowResLines [ j ] ) [ 'RESOLUTION' ] ;
if ( res && lowResLines [ j + 1 ] . endsWith ( '.m3u8' ) ) {
// Assumes resolutions are correctly ordered
lowResBestUrl = lowResLines [ j + 1 ] ;
break ;
}
}
}
if ( lowResBestUrl != null && streamInfo . Encodings != null ) {
var normalEncodingsM3u8 = streamInfo . Encodings ;
var normalLines = normalEncodingsM3u8 . replace ( '\r' , '' ) . split ( '\n' ) ;
for ( var j = 0 ; j < normalLines . length - 1 ; j ++ ) {
if ( normalLines [ j ] . startsWith ( '#EXT-X-STREAM-INF' ) ) {
var res = parseAttributes ( normalLines [ j ] ) [ 'RESOLUTION' ] ;
if ( res ) {
lowResBestUrl += ' ' ; // The stream doesn't load unless each url line is unique
normalLines [ j + 1 ] = lowResBestUrl ;
}
}
}
encodingsM3u8 = normalLines . join ( '\r\n' ) ;
}
2022-11-19 21:47:54 +01:00
streamInfo . BackupEncodings = encodingsM3u8 ;
}
var lines = encodingsM3u8 . replace ( '\r' , '' ) . split ( '\n' ) ;
for ( var j = 0 ; j < lines . length ; j ++ ) {
if ( ! lines [ j ] . startsWith ( '#' ) && lines [ j ] . includes ( '.m3u8' ) ) {
StreamInfosByUrl [ lines [ j ] . trimEnd ( ) ] = streamInfo ;
}
}
} else {
resolve ( encodingsM3u8Response ) ;
return ;
}
}
}
if ( streamInfo . UseBackupStream ) {
resolve ( new Response ( streamInfo . BackupEncodings ) ) ;
} else {
resolve ( new Response ( streamInfo . Encodings ) ) ;
}
} ) ;
}
}
}
return realFetch . apply ( this , arguments ) ;
}
}
function makeGraphQlPacket ( event , radToken , payload ) {
return [ {
operationName : 'ClientSideAdEventHandling_RecordAdEvent' ,
variables : {
input : {
eventName : event ,
eventPayload : JSON . stringify ( payload ) ,
radToken ,
} ,
} ,
extensions : {
persistedQuery : {
version : 1 ,
sha256Hash : '7e6c69e6eb59f8ccb97ab73686f3d8b7d85a72a0298745ccd8bfc68e4054ca5b' ,
} ,
} ,
} ] ;
}
2023-06-01 10:21:49 +02:00
function getAccessToken ( channelName , playerType , platform , realFetch ) {
if ( ! platform ) {
platform = 'web' ;
}
2022-11-19 21:47:54 +01:00
var body = null ;
2023-06-01 10:21:49 +02:00
var templateQuery = 'query PlaybackAccessToken_Template($login: String!, $isLive: Boolean!, $vodID: ID!, $isVod: Boolean!, $playerType: String!) { streamPlaybackAccessToken(channelName: $login, params: {platform: "' + platform + '", playerBackend: "mediaplayer", playerType: $playerType}) @include(if: $isLive) { value signature __typename } videoPlaybackAccessToken(id: $vodID, params: {platform: "' + platform + '", playerBackend: "mediaplayer", playerType: $playerType}) @include(if: $isVod) { value signature __typename }}' ;
2022-11-19 21:47:54 +01:00
body = {
operationName : 'PlaybackAccessToken_Template' ,
query : templateQuery ,
variables : {
'isLive' : true ,
'login' : channelName ,
'isVod' : false ,
'vodID' : '' ,
'playerType' : playerType
}
} ;
return gqlRequest ( body , realFetch ) ;
}
function gqlRequest ( body , realFetch ) {
2023-06-01 11:33:30 +02:00
if ( ClientIntegrityHeader == null ) {
2023-10-11 12:08:30 +02:00
//console.warn('ClientIntegrityHeader is null');
2023-06-02 00:16:23 +02:00
//throw 'ClientIntegrityHeader is null';
2023-06-01 09:03:45 +02:00
}
2022-11-19 21:47:54 +01:00
var fetchFunc = realFetch ? realFetch : fetch ;
return fetchFunc ( 'https://gql.twitch.tv/gql' , {
method : 'POST' ,
body : JSON . stringify ( body ) ,
headers : {
2023-06-01 09:03:45 +02:00
'Client-Id' : CLIENT _ID ,
2023-06-01 11:33:30 +02:00
'Client-Integrity' : ClientIntegrityHeader ,
'X-Device-Id' : OPT _ROLLING _DEVICE _ID ? gql _device _id _rolling : gql _device _id ,
'Authorization' : AuthorizationHeader
2022-11-19 21:47:54 +01:00
}
} ) ;
}
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 tryNotifyAdsWatchedM3U8 ( streamM3u8 ) {
try {
//console.log(streamM3u8);
if ( ! streamM3u8 || ! streamM3u8 . includes ( AD _SIGNIFIER ) ) {
return 1 ;
}
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 : false ,
player _volume : 0.5 ,
visible : true ,
} ;
for ( let podPosition = 0 ; podPosition < podLength ; podPosition ++ ) {
if ( OPT _MODE _NOTIFY _ADS _WATCHED _MIN _REQUESTS ) {
// This is all that's actually required at the moment
await gqlRequest ( makeGraphQlPacket ( 'video_ad_pod_complete' , radToken , baseData ) ) ;
} else {
const extendedData = {
... baseData ,
ad _id : adId ,
ad _position : podPosition ,
duration : 30 ,
creative _id : creativeId ,
total _ads : podLength ,
order _id : orderId ,
line _item _id : lineItemId ,
} ;
await gqlRequest ( makeGraphQlPacket ( 'video_ad_impression' , radToken , extendedData ) ) ;
for ( let quartile = 0 ; quartile < 4 ; quartile ++ ) {
await gqlRequest (
makeGraphQlPacket ( 'video_ad_quartile_complete' , radToken , {
... extendedData ,
quartile : quartile + 1 ,
} )
) ;
}
await gqlRequest ( makeGraphQlPacket ( 'video_ad_pod_complete' , radToken , baseData ) ) ;
}
}
}
return 0 ;
} catch ( err ) {
console . log ( err ) ;
return 0 ;
}
}
function hookFetch ( ) {
var realFetch = window . fetch ;
window . fetch = function ( url , init , ... args ) {
if ( typeof url === 'string' ) {
if ( url . includes ( 'gql' ) ) {
var deviceId = init . headers [ 'X-Device-Id' ] ;
if ( typeof deviceId !== 'string' ) {
deviceId = init . headers [ 'Device-ID' ] ;
}
if ( typeof deviceId === 'string' ) {
gql _device _id = deviceId ;
}
if ( gql _device _id && twitchMainWorker ) {
twitchMainWorker . postMessage ( {
key : 'UboUpdateDeviceId' ,
value : gql _device _id
} ) ;
}
if ( typeof init . body === 'string' && init . body . includes ( 'PlaybackAccessToken' ) ) {
if ( OPT _ACCESS _TOKEN _PLAYER _TYPE ) {
const newBody = JSON . parse ( init . body ) ;
2023-06-01 09:03:45 +02:00
if ( Array . isArray ( newBody ) ) {
for ( let i = 0 ; i < newBody . length ; i ++ ) {
newBody [ i ] . variables . playerType = OPT _ACCESS _TOKEN _PLAYER _TYPE ;
}
} else {
newBody . variables . playerType = OPT _ACCESS _TOKEN _PLAYER _TYPE ;
}
2022-11-19 21:47:54 +01:00
init . body = JSON . stringify ( newBody ) ;
}
if ( OPT _ROLLING _DEVICE _ID ) {
if ( typeof init . headers [ 'X-Device-Id' ] === 'string' ) {
init . headers [ 'X-Device-Id' ] = gql _device _id _rolling ;
}
if ( typeof init . headers [ 'Device-ID' ] === 'string' ) {
init . headers [ 'Device-ID' ] = gql _device _id _rolling ;
}
}
2023-06-01 09:03:45 +02:00
if ( typeof init . headers [ 'Client-Integrity' ] === 'string' ) {
2023-06-01 11:33:30 +02:00
ClientIntegrityHeader = init . headers [ 'Client-Integrity' ] ;
2023-10-11 12:08:30 +02:00
if ( ClientIntegrityHeader && twitchMainWorker ) {
twitchMainWorker . postMessage ( {
key : 'UpdateClientIntegrityHeader' ,
value : init . headers [ 'Client-Integrity' ]
} ) ;
}
2023-06-01 11:33:30 +02:00
}
if ( typeof init . headers [ 'Authorization' ] === 'string' ) {
AuthorizationHeader = init . headers [ 'Authorization' ] ;
2023-10-11 12:08:30 +02:00
if ( AuthorizationHeader && twitchMainWorker ) {
twitchMainWorker . postMessage ( {
key : 'UpdateAuthorizationHeader' ,
value : init . headers [ 'Authorization' ]
} ) ;
}
2023-06-01 09:03:45 +02:00
}
2022-11-19 21:47:54 +01:00
}
}
}
return realFetch . apply ( this , arguments ) ;
} ;
}
function reloadTwitchPlayer ( isSeek , isPausePlay ) {
// Taken from ttv-tools / ffz
// https://github.com/Nerixyz/ttv-tools/blob/master/src/context/twitch-player.ts
// https://github.com/FrankerFaceZ/FrankerFaceZ/blob/master/src/sites/twitch-twilight/modules/player.jsx
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 ;
}
2024-02-23 12:23:33 +01:00
function findReactRootNode ( ) {
var reactRootNode = null ;
var rootNode = document . querySelector ( '#root' ) ;
if ( rootNode && rootNode . _reactRootContainer && rootNode . _reactRootContainer . _internalRoot && rootNode . _reactRootContainer . _internalRoot . current ) {
reactRootNode = rootNode . _reactRootContainer . _internalRoot . current ;
}
if ( reactRootNode == null ) {
var containerName = Object . keys ( rootNode ) . find ( x => x . startsWith ( '__reactContainer' ) ) ;
if ( containerName != null ) {
reactRootNode = rootNode [ containerName ] ;
}
}
return reactRootNode ;
2022-11-19 21:47:54 +01:00
}
2024-02-23 12:23:33 +01:00
var reactRootNode = findReactRootNode ( ) ;
2022-11-19 21:47:54 +01:00
if ( ! reactRootNode ) {
console . log ( 'Could not find react root' ) ;
return ;
}
var player = findReactNode ( reactRootNode , node => node . setPlayerActive && node . props && node . props . mediaPlayerInstance ) ;
player = player && player . props && player . props . mediaPlayerInstance ? player . props . mediaPlayerInstance : null ;
var playerState = findReactNode ( reactRootNode , node => node . setSrc && node . setInitialPlaybackSettings ) ;
if ( ! player ) {
console . log ( 'Could not find player' ) ;
return ;
}
if ( ! playerState ) {
console . log ( 'Could not find player state' ) ;
return ;
}
2024-02-23 12:23:33 +01:00
if ( player . paused || player . core ? . paused ) {
2022-11-19 21:47:54 +01:00
return ;
}
if ( isSeek ) {
console . log ( 'Force seek to reset player (hopefully fixing any audio desync) pos:' + player . getPosition ( ) + ' range:' + JSON . stringify ( player . getBuffered ( ) ) ) ;
var pos = player . getPosition ( ) ;
player . seekTo ( 0 ) ;
player . seekTo ( pos ) ;
return ;
}
if ( isPausePlay ) {
player . pause ( ) ;
player . play ( ) ;
return ;
}
2023-08-09 02:07:38 +02:00
const lsKeyQuality = 'video-quality' ;
const lsKeyMuted = 'video-muted' ;
const lsKeyVolume = 'volume' ;
var currentQualityLS = localStorage . getItem ( lsKeyQuality ) ;
var currentMutedLS = localStorage . getItem ( lsKeyMuted ) ;
var currentVolumeLS = localStorage . getItem ( lsKeyVolume ) ;
if ( player ? . core ? . state ) {
localStorage . setItem ( lsKeyMuted , JSON . stringify ( { default : player . core . state . muted } ) ) ;
localStorage . setItem ( lsKeyVolume , player . core . state . volume ) ;
}
if ( player ? . core ? . state ? . quality ? . group ) {
localStorage . setItem ( lsKeyQuality , JSON . stringify ( { default : player . core . state . quality . group } ) ) ;
2022-11-19 21:47:54 +01:00
}
2023-08-09 02:07:38 +02:00
playerState . setSrc ( { isNewMediaPlayerInstance : true , refreshAccessToken : true } ) ;
setTimeout ( ( ) => {
localStorage . setItem ( lsKeyQuality , currentQualityLS ) ;
localStorage . setItem ( lsKeyMuted , currentMutedLS ) ;
localStorage . setItem ( lsKeyVolume , currentVolumeLS ) ;
} , 3000 ) ;
2022-11-19 21:47:54 +01:00
}
window . reloadTwitchPlayer = reloadTwitchPlayer ;
hookFetch ( ) ;
function onContentLoaded ( ) {
// This stops Twitch from pausing the player when in another tab and an ad shows.
// Taken from https://github.com/saucettv/VideoAdBlockForTwitch/blob/cefce9d2b565769c77e3666ac8234c3acfe20d83/chrome/content.js#L30
try {
Object . defineProperty ( document , 'visibilityState' , {
get ( ) {
return 'visible' ;
}
} ) ;
} catch { }
try {
Object . defineProperty ( document , 'hidden' , {
get ( ) {
return false ;
}
} ) ;
} catch { }
var block = e => {
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
e . stopImmediatePropagation ( ) ;
} ;
document . addEventListener ( 'visibilitychange' , block , true ) ;
document . addEventListener ( 'webkitvisibilitychange' , block , true ) ;
document . addEventListener ( 'mozvisibilitychange' , block , true ) ;
document . addEventListener ( 'hasFocus' , block , true ) ;
try {
if ( /Firefox/ . test ( navigator . userAgent ) ) {
Object . defineProperty ( document , 'mozHidden' , {
get ( ) {
return false ;
}
} ) ;
} else {
Object . defineProperty ( document , 'webkitHidden' , {
get ( ) {
return false ;
}
} ) ;
}
} catch { }
// Hooks for preserving volume / resolution
var keysToCache = [
'video-quality' ,
'video-muted' ,
'volume' ,
'lowLatencyModeEnabled' , // Low Latency
'persistenceEnabled' , // Mini Player
] ;
var cachedValues = new Map ( ) ;
for ( var i = 0 ; i < keysToCache . length ; i ++ ) {
cachedValues . set ( keysToCache [ i ] , localStorage . getItem ( keysToCache [ i ] ) ) ;
}
var realSetItem = localStorage . setItem ;
localStorage . setItem = function ( key , value ) {
if ( cachedValues . has ( key ) ) {
cachedValues . set ( key , value ) ;
}
realSetItem . apply ( this , arguments ) ;
} ;
var realGetItem = localStorage . getItem ;
localStorage . getItem = function ( key ) {
if ( cachedValues . has ( key ) ) {
return cachedValues . get ( key ) ;
}
return realGetItem . apply ( this , arguments ) ;
} ;
}
if ( document . readyState === "complete" || document . readyState === "loaded" || document . readyState === "interactive" ) {
onContentLoaded ( ) ;
} else {
window . addEventListener ( "DOMContentLoaded" , function ( ) {
onContentLoaded ( ) ;
} ) ;
}
} ) ( ) ;