lyrical

Untitled

Sep 1st, 2024
287
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. // ==UserScript==
  2. // @name         TwitchAdSolutions (video-swap-new)
  3. // @namespace    https://github.com/pixeltris/TwitchAdSolutions
  4. // @version      1.30
  5. // @updateURL    https://github.com/pixeltris/TwitchAdSolutions/raw/master/video-swap-new/video-swap-new.user.js
  6. // @downloadURL  https://github.com/pixeltris/TwitchAdSolutions/raw/master/video-swap-new/video-swap-new.user.js
  7. // @description  Multiple solutions for blocking Twitch ads (video-swap-new)
  8. // @author       pixeltris
  9. // @match        *://*.twitch.tv/*
  10. // @run-at       document-start
  11. // @inject-into  page
  12. // @grant        none
  13. // ==/UserScript==
  14. (function() {
  15.     'use strict';
  16.     function declareOptions(scope) {
  17.         // Options / globals
  18.         scope.OPT_MODE_STRIP_AD_SEGMENTS = true;
  19.         scope.OPT_MODE_NOTIFY_ADS_WATCHED = true;
  20.         scope.OPT_MODE_NOTIFY_ADS_WATCHED_MIN_REQUESTS = false;
  21.         scope.OPT_BACKUP_PLAYER_TYPE = 'autoplay';
  22.         scope.OPT_BACKUP_PLATFORM = 'ios';
  23.         scope.OPT_REGULAR_PLAYER_TYPE = 'site';
  24.         scope.OPT_ACCESS_TOKEN_PLAYER_TYPE = null;
  25.         scope.OPT_SHOW_AD_BANNER = true;
  26.         scope.AD_SIGNIFIER = 'stitched-ad';
  27.         scope.LIVE_SIGNIFIER = ',live';
  28.         scope.CLIENT_ID = 'kimne78kx3ncx6brgo4mv6wki5h1ko';
  29.         // These are only really for Worker scope...
  30.         scope.StreamInfos = [];
  31.         scope.StreamInfosByUrl = [];
  32.         scope.CurrentChannelNameFromM3U8 = null;
  33.         // Need this in both scopes. Window scope needs to update this to worker scope.
  34.         scope.gql_device_id = null;
  35.         scope.ClientIntegrityHeader = null;
  36.         scope.AuthorizationHeader = null;
  37.     }
  38.     var twitchWorkers = [];
  39.     const oldWorker = window.Worker;
  40.     function hookWindowWorker() {
  41.         window.Worker = class Worker extends oldWorker {
  42.             constructor(twitchBlobUrl, options) {
  43.                 var isTwitchWorker = false;
  44.                 try {
  45.                     isTwitchWorker = new URL(twitchBlobUrl).origin.endsWith('.twitch.tv');
  46.                 } catch {}
  47.                 if (!isTwitchWorker) {
  48.                     super(twitchBlobUrl, options);
  49.                     return;
  50.                 }
  51.                 var newBlobStr = `
  52.                     ${processM3U8.toString()}
  53.                     ${hookWorkerFetch.toString()}
  54.                     ${declareOptions.toString()}
  55.                     ${getAccessToken.toString()}
  56.                     ${gqlRequest.toString()}
  57.                     ${makeGraphQlPacket.toString()}
  58.                     ${tryNotifyAdsWatchedM3U8.toString()}
  59.                     ${parseAttributes.toString()}
  60.                     ${onFoundAd.toString()}
  61.                     ${getWasmWorkerUrl.toString()}
  62.                     var workerUrl = getWasmWorkerUrl('${twitchBlobUrl.replaceAll("'", "%27")}');
  63.                    if (workerUrl && workerUrl.includes('assets.twitch.tv/assets/amazon-ivs-wasmworker')) {
  64.                        declareOptions(self);
  65.                        self.addEventListener('message', function(e) {
  66.                            if (e.data.key == 'UboUpdateDeviceId') {
  67.                                gql_device_id = e.data.value;
  68.                            } else if (e.data.key == 'UpdateClientIntegrityHeader') {
  69.                                ClientIntegrityHeader = e.data.value;
  70.                            } else if (e.data.key == 'UpdateAuthorizationHeader') {
  71.                                AuthorizationHeader = e.data.value;
  72.                            }
  73.                        });
  74.                        hookWorkerFetch();
  75.                        importScripts(workerUrl);
  76.                    }
  77.                `
  78.                super(URL.createObjectURL(new Blob([newBlobStr])), options);
  79.                twitchWorkers.push(this);
  80.                this.onmessage = function(e) {
  81.                    // NOTE: Removed adDiv caching as '.video-player' can change between streams?
  82.                    if (e.data.key == 'UboShowAdBanner') {
  83.                        var adDiv = getAdDiv();
  84.                        if (adDiv != null) {
  85.                            adDiv.P.textContent = 'Blocking' + (e.data.isMidroll ? ' midroll' : '') + ' ads';
  86.                            if (OPT_SHOW_AD_BANNER) {
  87.                                adDiv.style.display = 'block';
  88.                            }
  89.                        }
  90.                    } else if (e.data.key == 'UboHideAdBanner') {
  91.                        var adDiv = getAdDiv();
  92.                        if (adDiv != null) {
  93.                            adDiv.style.display = 'none';
  94.                        }
  95.                    } else if (e.data.key == 'UboChannelNameM3U8Changed') {
  96.                        //console.log('M3U8 channel name changed to ' + e.data.value);
  97.                    } else if (e.data.key == 'UboReloadPlayer') {
  98.                        reloadTwitchPlayer();
  99.                    } else if (e.data.key == 'UboPauseResumePlayer') {
  100.                        reloadTwitchPlayer(false, true);
  101.                    } else if (e.data.key == 'UboSeekPlayer') {
  102.                        reloadTwitchPlayer(true);
  103.                    }
  104.                }
  105.                function getAdDiv() {
  106.                    var playerRootDiv = document.querySelector('.video-player');
  107.                    var adDiv = null;
  108.                    if (playerRootDiv != null) {
  109.                        adDiv = playerRootDiv.querySelector('.ubo-overlay');
  110.                        if (adDiv == null) {
  111.                            adDiv = document.createElement('div');
  112.                            adDiv.className = 'ubo-overlay';
  113.                            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>';
  114.                            adDiv.style.display = 'none';
  115.                            adDiv.P = adDiv.querySelector('p');
  116.                            playerRootDiv.appendChild(adDiv);
  117.                        }
  118.                    }
  119.                    return adDiv;
  120.                }
  121.            }
  122.        }
  123.    }
  124.    function getWasmWorkerUrl(twitchBlobUrl) {
  125.        var req = new XMLHttpRequest();
  126.        req.open('GET', twitchBlobUrl, false);
  127.        req.overrideMimeType("text/javascript");
  128.        req.send();
  129.        return req.responseText.split("'")[1];
  130.    }
  131.    function onFoundAd(streamInfo, textStr, reloadPlayer) {
  132.        console.log('Found ads, switch to backup');
  133.        streamInfo.UseBackupStream = true;
  134.        streamInfo.IsMidroll = textStr.includes('"MIDROLL"') || textStr.includes('"midroll"');
  135.        if (reloadPlayer) {
  136.            postMessage({key:'UboReloadPlayer'});
  137.        }
  138.        postMessage({key:'UboShowAdBanner',isMidroll:streamInfo.IsMidroll});
  139.    }
  140.    async function processM3U8(url, textStr, realFetch) {
  141.        var streamInfo = StreamInfosByUrl[url];
  142.        if (streamInfo == null) {
  143.            console.log('Unknown stream url ' + url);
  144.            //postMessage({key:'UboHideAdBanner'});
  145.            return textStr;
  146.        }
  147.        if (!OPT_MODE_STRIP_AD_SEGMENTS) {
  148.            return textStr;
  149.        }
  150.        var haveAdTags = textStr.includes(AD_SIGNIFIER);
  151.        if (streamInfo.UseBackupStream) {
  152.            if (streamInfo.Encodings == null) {
  153.                console.log('Found backup stream but not main stream?');
  154.                streamInfo.UseBackupStream = false;
  155.                postMessage({key:'UboReloadPlayer'});
  156.                return '';
  157.            } else {
  158.                var streamM3u8Url = streamInfo.Encodings.match(/^https:.*\.m3u8$/m)[0];
  159.                var streamM3u8Response = await realFetch(streamM3u8Url);
  160.                if (streamM3u8Response.status == 200) {
  161.                    var streamM3u8 = await streamM3u8Response.text();
  162.                    if (streamM3u8 != null) {
  163.                        if (!streamM3u8.includes(AD_SIGNIFIER)) {
  164.                            console.log('No more ads on main stream. Triggering player reload to go back to main stream...');
  165.                            streamInfo.UseBackupStream = false;
  166.                            postMessage({key:'UboHideAdBanner'});
  167.                            postMessage({key:'UboReloadPlayer'});
  168.                        } else if (!streamM3u8.includes('"MIDROLL"') && !streamM3u8.includes('"midroll"')) {
  169.                            var lines = streamM3u8.replace('\r', '').split('\n');
  170.                            for (var i = 0; i < lines.length; i++) {
  171.                                var line = lines[i];
  172.                                if (line.startsWith('#EXTINF') && lines.length > i + 1) {
  173.                                    if (!line.includes(LIVE_SIGNIFIER) && !streamInfo.RequestedAds.has(lines[i + 1])) {
  174.                                        // Only request one .ts file per .m3u8 request to avoid making too many requests
  175.                                        //console.log('Fetch ad .ts file');
  176.                                        streamInfo.RequestedAds.add(lines[i + 1]);
  177.                                        fetch(lines[i + 1]).then((response)=>{response.blob()});
  178.                                        break;
  179.                                    }
  180.                                }
  181.                            }
  182.                        }
  183.                    }
  184.                }
  185.            }
  186.        } else if (haveAdTags) {
  187.            onFoundAd(streamInfo, textStr, true);
  188.        } else {
  189.            postMessage({key:'UboHideAdBanner'});
  190.        }
  191.        if (haveAdTags && streamInfo.BackupEncodings != null) {
  192.            var streamM3u8Url = streamInfo.BackupEncodings.match(/^https:.*\.m3u8.*$/m)[0];
  193.            var streamM3u8Response = await realFetch(streamM3u8Url);
  194.            if (streamM3u8Response.status == 200) {
  195.                textStr = await streamM3u8Response.text();
  196.            }
  197.        }
  198.        return textStr;
  199.    }
  200.    function hookWorkerFetch() {
  201.        console.log('hookWorkerFetch');
  202.        var realFetch = fetch;
  203.        fetch = async function(url, options) {
  204.            if (typeof url === 'string') {
  205.                url = url.trimEnd();
  206.                if (url.endsWith('m3u8')) {
  207.                    return new Promise(function(resolve, reject) {
  208.                        var processAfter = async function(response) {
  209.                            var str = await processM3U8(url, await response.text(), realFetch);
  210.                            resolve(new Response(str, {
  211.                                status: response.status,
  212.                                statusText: response.statusText,
  213.                                headers: response.headers
  214.                            }));
  215.                        };
  216.                        var send = function() {
  217.                            return realFetch(url, options).then(function(response) {
  218.                                processAfter(response);
  219.                            })['catch'](function(err) {
  220.                                console.log('fetch hook err ' + err);
  221.                                reject(err);
  222.                            });
  223.                        };
  224.                        send();
  225.                    });
  226.                }
  227.                else if (url.includes('/api/channel/hls/') && !url.includes('picture-by-picture')) {
  228.                    var channelName = (new URL(url)).pathname.match(/([^\/]+)(?=\.\w+$)/)[0];
  229.                    if (CurrentChannelNameFromM3U8 != channelName) {
  230.                        postMessage({
  231.                            key: 'UboChannelNameM3U8Changed',
  232.                            value: channelName
  233.                        });
  234.                    }
  235.                    CurrentChannelNameFromM3U8 = channelName;
  236.                    if (OPT_MODE_STRIP_AD_SEGMENTS) {
  237.                        return new Promise(async function(resolve, reject) {
  238.                            // - First m3u8 request is the m3u8 with the video encodings (360p,480p,720p,etc).
  239.                            // - 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.
  240.                             var streamInfo = StreamInfos[channelName];
  241.                             if (streamInfo != null && streamInfo.Encodings != null && (await realFetch(streamInfo.Encodings.match(/^https:.*\.m3u8$/m)[0])).status !== 200) {
  242.                                 // The cached encodings are dead (the stream probably restarted)
  243.                                 streamInfo = null;
  244.                             }
  245.                             if (streamInfo == null || streamInfo.Encodings == null || streamInfo.BackupEncodings == null) {
  246.                                 StreamInfos[channelName] = streamInfo = {
  247.                                     RequestedAds: new Set(),
  248.                                     Encodings: null,
  249.                                     BackupEncodings: null,
  250.                                     IsMidroll: false,
  251.                                     UseBackupStream: false,
  252.                                     ChannelName: channelName
  253.                                 };
  254.                                 for (var i = 0; i < 2; i++) {
  255.                                     var encodingsUrl = url;
  256.                                     if (i == 1) {
  257.                                         var accessTokenResponse = await getAccessToken(channelName, OPT_BACKUP_PLAYER_TYPE, OPT_BACKUP_PLATFORM, realFetch);
  258.                                         if (accessTokenResponse != null && accessTokenResponse.status === 200) {
  259.                                             var accessToken = await accessTokenResponse.json();
  260.                                             var urlInfo = new URL('https://usher.ttvnw.net/api/channel/hls/' + channelName + '.m3u8' + (new URL(url)).search);
  261.                                             urlInfo.searchParams.set('sig', accessToken.data.streamPlaybackAccessToken.signature);
  262.                                             urlInfo.searchParams.set('token', accessToken.data.streamPlaybackAccessToken.value);
  263.                                             encodingsUrl = urlInfo.href;
  264.                                         } else {
  265.                                             resolve(accessTokenResponse);
  266.                                             return;
  267.                                         }
  268.                                     }
  269.                                     var encodingsM3u8Response = await realFetch(encodingsUrl, options);
  270.                                     if (encodingsM3u8Response != null && encodingsM3u8Response.status === 200) {
  271.                                         var encodingsM3u8 = await encodingsM3u8Response.text();
  272.                                         if (i == 0) {
  273.                                             streamInfo.Encodings = encodingsM3u8;
  274.                                             var streamM3u8Url = encodingsM3u8.match(/^https:.*\.m3u8$/m)[0];
  275.                                             var streamM3u8Response = await realFetch(streamM3u8Url);
  276.                                             if (streamM3u8Response.status == 200) {
  277.                                                 var streamM3u8 = await streamM3u8Response.text();
  278.                                                 if (streamM3u8.includes(AD_SIGNIFIER)) {
  279.                                                     onFoundAd(streamInfo, streamM3u8, false);
  280.                                                 }
  281.                                             } else {
  282.                                                 resolve(streamM3u8Response);
  283.                                                 return;
  284.                                             }
  285.                                         } else {
  286.                                             var lowResLines = encodingsM3u8.replace('\r', '').split('\n');
  287.                                             var lowResBestUrl = null;
  288.                                             for (var j = 0; j < lowResLines.length; j++) {
  289.                                                 if (lowResLines[j].startsWith('#EXT-X-STREAM-INF')) {
  290.                                                     var res = parseAttributes(lowResLines[j])['RESOLUTION'];
  291.                                                     if (res && lowResLines[j + 1].endsWith('.m3u8')) {
  292.                                                         // Assumes resolutions are correctly ordered
  293.                                                         lowResBestUrl = lowResLines[j + 1];
  294.                                                         break;
  295.                                                     }
  296.                                                 }
  297.                                             }
  298.                                             if (lowResBestUrl != null && streamInfo.Encodings != null) {
  299.                                                 var normalEncodingsM3u8 = streamInfo.Encodings;
  300.                                                 var normalLines = normalEncodingsM3u8.replace('\r', '').split('\n');
  301.                                                 for (var j = 0; j < normalLines.length - 1; j++) {
  302.                                                     if (normalLines[j].startsWith('#EXT-X-STREAM-INF')) {
  303.                                                         var res = parseAttributes(normalLines[j])['RESOLUTION'];
  304.                                                         if (res) {
  305.                                                             lowResBestUrl += ' ';// The stream doesn't load unless each url line is unique
  306.                                                             normalLines[j + 1] = lowResBestUrl;
  307.                                                         }
  308.                                                     }
  309.                                                 }
  310.                                                 encodingsM3u8 = normalLines.join('\r\n');
  311.                                             }
  312.                                             streamInfo.BackupEncodings = encodingsM3u8;
  313.                                         }
  314.                                         var lines = encodingsM3u8.replace('\r', '').split('\n');
  315.                                         for (var j = 0; j < lines.length; j++) {
  316.                                             if (!lines[j].startsWith('#') && lines[j].includes('.m3u8')) {
  317.                                                 StreamInfosByUrl[lines[j].trimEnd()] = streamInfo;
  318.                                             }
  319.                                         }
  320.                                     } else {
  321.                                         resolve(encodingsM3u8Response);
  322.                                         return;
  323.                                     }
  324.                                 }
  325.                             }
  326.                             if (streamInfo.UseBackupStream) {
  327.                                 resolve(new Response(streamInfo.BackupEncodings));
  328.                             } else {
  329.                                 resolve(new Response(streamInfo.Encodings));
  330.                             }
  331.                         });
  332.                     }
  333.                 }
  334.             }
  335.             return realFetch.apply(this, arguments);
  336.         }
  337.     }
  338.     function makeGraphQlPacket(event, radToken, payload) {
  339.         return [{
  340.             operationName: 'ClientSideAdEventHandling_RecordAdEvent',
  341.             variables: {
  342.                 input: {
  343.                     eventName: event,
  344.                     eventPayload: JSON.stringify(payload),
  345.                     radToken,
  346.                 },
  347.             },
  348.             extensions: {
  349.                 persistedQuery: {
  350.                     version: 1,
  351.                     sha256Hash: '7e6c69e6eb59f8ccb97ab73686f3d8b7d85a72a0298745ccd8bfc68e4054ca5b',
  352.                 },
  353.             },
  354.         }];
  355.     }
  356.     function getAccessToken(channelName, playerType, platform, realFetch) {
  357.         if (!platform) {
  358.             platform = 'web';
  359.         }
  360.         var body = null;
  361.         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  }}';
  362.         body = {
  363.             operationName: 'PlaybackAccessToken_Template',
  364.             query: templateQuery,
  365.             variables: {
  366.                 'isLive': true,
  367.                 'login': channelName,
  368.                 'isVod': false,
  369.                 'vodID': '',
  370.                 'playerType': playerType
  371.             }
  372.         };
  373.         return gqlRequest(body, realFetch);
  374.     }
  375.     function gqlRequest(body, realFetch) {
  376.         if (ClientIntegrityHeader == null) {
  377.             //console.warn('ClientIntegrityHeader is null');
  378.             //throw 'ClientIntegrityHeader is null';
  379.         }
  380.         var fetchFunc = realFetch ? realFetch : fetch;
  381.         return fetchFunc('https://gql.twitch.tv/gql', {
  382.             method: 'POST',
  383.             body: JSON.stringify(body),
  384.             headers: {
  385.                 'Client-Id': CLIENT_ID,
  386.                 'Client-Integrity': ClientIntegrityHeader,
  387.                 'X-Device-Id': gql_device_id,
  388.                 'Authorization': AuthorizationHeader
  389.             }
  390.         });
  391.     }
  392.     function parseAttributes(str) {
  393.         return Object.fromEntries(
  394.             str.split(/(?:^|,)((?:[^=]*)=(?:"[^"]*"|[^,]*))/)
  395.                 .filter(Boolean)
  396.                 .map(x => {
  397.                     const idx = x.indexOf('=');
  398.                     const key = x.substring(0, idx);
  399.                     const value = x.substring(idx +1);
  400.                     const num = Number(value);
  401.                     return [key, Number.isNaN(num) ? value.startsWith('"') ? JSON.parse(value) : value : num]
  402.                 }));
  403.     }
  404.     async function tryNotifyAdsWatchedM3U8(streamM3u8) {
  405.         try {
  406.             //console.log(streamM3u8);
  407.             if (!streamM3u8 || !streamM3u8.includes(AD_SIGNIFIER)) {
  408.                 return 1;
  409.             }
  410.             var matches = streamM3u8.match(/#EXT-X-DATERANGE:(ID="stitched-ad-[^\n]+)\n/);
  411.             if (matches.length > 1) {
  412.                 const attrString = matches[1];
  413.                 const attr = parseAttributes(attrString);
  414.                 var podLength = parseInt(attr['X-TV-TWITCH-AD-POD-LENGTH'] ? attr['X-TV-TWITCH-AD-POD-LENGTH'] : '1');
  415.                 var podPosition = parseInt(attr['X-TV-TWITCH-AD-POD-POSITION'] ? attr['X-TV-TWITCH-AD-POD-POSITION'] : '0');
  416.                 var radToken = attr['X-TV-TWITCH-AD-RADS-TOKEN'];
  417.                 var lineItemId = attr['X-TV-TWITCH-AD-LINE-ITEM-ID'];
  418.                 var orderId = attr['X-TV-TWITCH-AD-ORDER-ID'];
  419.                 var creativeId = attr['X-TV-TWITCH-AD-CREATIVE-ID'];
  420.                 var adId = attr['X-TV-TWITCH-AD-ADVERTISER-ID'];
  421.                 var rollType = attr['X-TV-TWITCH-AD-ROLL-TYPE'].toLowerCase();
  422.                 const baseData = {
  423.                     stitched: true,
  424.                     roll_type: rollType,
  425.                     player_mute: false,
  426.                     player_volume: 0.5,
  427.                     visible: true,
  428.                 };
  429.                 for (let podPosition = 0; podPosition < podLength; podPosition++) {
  430.                     if (OPT_MODE_NOTIFY_ADS_WATCHED_MIN_REQUESTS) {
  431.                         // This is all that's actually required at the moment
  432.                         await gqlRequest(makeGraphQlPacket('video_ad_pod_complete', radToken, baseData));
  433.                     } else {
  434.                         const extendedData = {
  435.                             ...baseData,
  436.                             ad_id: adId,
  437.                             ad_position: podPosition,
  438.                             duration: 30,
  439.                             creative_id: creativeId,
  440.                             total_ads: podLength,
  441.                             order_id: orderId,
  442.                             line_item_id: lineItemId,
  443.                         };
  444.                         await gqlRequest(makeGraphQlPacket('video_ad_impression', radToken, extendedData));
  445.                         for (let quartile = 0; quartile < 4; quartile++) {
  446.                             await gqlRequest(
  447.                                 makeGraphQlPacket('video_ad_quartile_complete', radToken, {
  448.                                     ...extendedData,
  449.                                     quartile: quartile + 1,
  450.                                 })
  451.                             );
  452.                         }
  453.                         await gqlRequest(makeGraphQlPacket('video_ad_pod_complete', radToken, baseData));
  454.                     }
  455.                 }
  456.             }
  457.             return 0;
  458.         } catch (err) {
  459.             console.log(err);
  460.             return 0;
  461.         }
  462.     }
  463.     function postTwitchWorkerMessage(key, value) {
  464.         twitchWorkers.forEach((worker) => {
  465.             worker.postMessage({key: key, value: value});
  466.         });
  467.     }
  468.     function hookFetch() {
  469.         var realFetch = window.fetch;
  470.         window.fetch = function(url, init, ...args) {
  471.             if (typeof url === 'string') {
  472.                 if (url.includes('gql')) {
  473.                     var deviceId = init.headers['X-Device-Id'];
  474.                     if (typeof deviceId !== 'string') {
  475.                         deviceId = init.headers['Device-ID'];
  476.                     }
  477.                     if (typeof deviceId === 'string') {
  478.                         gql_device_id = deviceId;
  479.                     }
  480.                     if (gql_device_id) {
  481.                         postTwitchWorkerMessage('UboUpdateDeviceId', gql_device_id);
  482.                     }
  483.                     if (typeof init.body === 'string' && init.body.includes('PlaybackAccessToken')) {
  484.                         if (OPT_ACCESS_TOKEN_PLAYER_TYPE) {
  485.                             const newBody = JSON.parse(init.body);
  486.                             if (Array.isArray(newBody)) {
  487.                                 for (let i = 0; i < newBody.length; i++) {
  488.                                     newBody[i].variables.playerType = OPT_ACCESS_TOKEN_PLAYER_TYPE;
  489.                                 }
  490.                             } else {
  491.                                 newBody.variables.playerType = OPT_ACCESS_TOKEN_PLAYER_TYPE;
  492.                             }
  493.                             init.body = JSON.stringify(newBody);
  494.                         }
  495.                         if (typeof init.headers['Client-Integrity'] === 'string') {
  496.                             ClientIntegrityHeader = init.headers['Client-Integrity'];
  497.                             if (ClientIntegrityHeader) {
  498.                                 postTwitchWorkerMessage('UpdateClientIntegrityHeader', init.headers['Client-Integrity']);
  499.                             }
  500.                         }
  501.                         if (typeof init.headers['Authorization'] === 'string') {
  502.                             AuthorizationHeader = init.headers['Authorization'];
  503.                             if (AuthorizationHeader) {
  504.                                 postTwitchWorkerMessage('UpdateAuthorizationHeader', init.headers['Authorization']);
  505.                             }
  506.                         }
  507.                     }
  508.                 }
  509.             }
  510.             return realFetch.apply(this, arguments);
  511.         };
  512.     }
  513.     function reloadTwitchPlayer(isSeek, isPausePlay) {
  514.         // Taken from ttv-tools / ffz
  515.         // https://github.com/Nerixyz/ttv-tools/blob/master/src/context/twitch-player.ts
  516.         // https://github.com/FrankerFaceZ/FrankerFaceZ/blob/master/src/sites/twitch-twilight/modules/player.jsx
  517.         function findReactNode(root, constraint) {
  518.             if (root.stateNode && constraint(root.stateNode)) {
  519.                 return root.stateNode;
  520.             }
  521.             let node = root.child;
  522.             while (node) {
  523.                 const result = findReactNode(node, constraint);
  524.                 if (result) {
  525.                     return result;
  526.                 }
  527.                 node = node.sibling;
  528.             }
  529.             return null;
  530.         }
  531.         function findReactRootNode() {
  532.             var reactRootNode = null;
  533.             var rootNode = document.querySelector('#root');
  534.             if (rootNode && rootNode._reactRootContainer && rootNode._reactRootContainer._internalRoot && rootNode._reactRootContainer._internalRoot.current) {
  535.                 reactRootNode = rootNode._reactRootContainer._internalRoot.current;
  536.             }
  537.             if (reactRootNode == null) {
  538.                 var containerName = Object.keys(rootNode).find(x => x.startsWith('__reactContainer'));
  539.                 if (containerName != null) {
  540.                     reactRootNode = rootNode[containerName];
  541.                 }
  542.             }
  543.             return reactRootNode;
  544.         }
  545.         var reactRootNode = findReactRootNode();
  546.         if (!reactRootNode) {
  547.             console.log('Could not find react root');
  548.             return;
  549.         }
  550.         var player = findReactNode(reactRootNode, node => node.setPlayerActive && node.props && node.props.mediaPlayerInstance);
  551.         player = player && player.props && player.props.mediaPlayerInstance ? player.props.mediaPlayerInstance : null;
  552.         var playerState = findReactNode(reactRootNode, node => node.setSrc && node.setInitialPlaybackSettings);
  553.         if (!player) {
  554.             console.log('Could not find player');
  555.             return;
  556.         }
  557.         if (!playerState) {
  558.             console.log('Could not find player state');
  559.             return;
  560.         }
  561.         if (player.paused || player.core?.paused) {
  562.             return;
  563.         }
  564.         if (isSeek) {
  565.             console.log('Force seek to reset player (hopefully fixing any audio desync) pos:' + player.getPosition() + ' range:' + JSON.stringify(player.getBuffered()));
  566.             var pos = player.getPosition();
  567.             player.seekTo(0);
  568.             player.seekTo(pos);
  569.             return;
  570.         }
  571.         if (isPausePlay) {
  572.             player.pause();
  573.             player.play();
  574.             return;
  575.         }
  576.         const lsKeyQuality = 'video-quality';
  577.         const lsKeyMuted = 'video-muted';
  578.         const lsKeyVolume = 'volume';
  579.         var currentQualityLS = localStorage.getItem(lsKeyQuality);
  580.         var currentMutedLS = localStorage.getItem(lsKeyMuted);
  581.         var currentVolumeLS = localStorage.getItem(lsKeyVolume);
  582.         if (player?.core?.state) {
  583.             localStorage.setItem(lsKeyMuted, JSON.stringify({default:player.core.state.muted}));
  584.             localStorage.setItem(lsKeyVolume, player.core.state.volume);
  585.         }
  586.         if (player?.core?.state?.quality?.group) {
  587.             localStorage.setItem(lsKeyQuality, JSON.stringify({default:player.core.state.quality.group}));
  588.         }
  589.         playerState.setSrc({ isNewMediaPlayerInstance: true, refreshAccessToken: true });
  590.         setTimeout(() => {
  591.             localStorage.setItem(lsKeyQuality, currentQualityLS);
  592.             localStorage.setItem(lsKeyMuted, currentMutedLS);
  593.             localStorage.setItem(lsKeyVolume, currentVolumeLS);
  594.         }, 3000);
  595.     }
  596.     function onContentLoaded() {
  597.         // This stops Twitch from pausing the player when in another tab and an ad shows.
  598.         // Taken from https://github.com/saucettv/VideoAdBlockForTwitch/blob/cefce9d2b565769c77e3666ac8234c3acfe20d83/chrome/content.js#L30
  599.         try {
  600.             Object.defineProperty(document, 'visibilityState', {
  601.                 get() {
  602.                     return 'visible';
  603.                 }
  604.             });
  605.         }catch{}
  606.         try {
  607.             Object.defineProperty(document, 'hidden', {
  608.                 get() {
  609.                     return false;
  610.                 }
  611.             });
  612.         }catch{}
  613.         var block = e => {
  614.             e.preventDefault();
  615.             e.stopPropagation();
  616.             e.stopImmediatePropagation();
  617.         };
  618.         document.addEventListener('visibilitychange', block, true);
  619.         document.addEventListener('webkitvisibilitychange', block, true);
  620.         document.addEventListener('mozvisibilitychange', block, true);
  621.         document.addEventListener('hasFocus', block, true);
  622.         try {
  623.             if (/Firefox/.test(navigator.userAgent)) {
  624.                 Object.defineProperty(document, 'mozHidden', {
  625.                     get() {
  626.                         return false;
  627.                     }
  628.                 });
  629.             } else {
  630.                 Object.defineProperty(document, 'webkitHidden', {
  631.                     get() {
  632.                         return false;
  633.                     }
  634.                 });
  635.             }
  636.         }catch{}
  637.         // Hooks for preserving volume / resolution
  638.         var keysToCache = [
  639.             'video-quality',
  640.             'video-muted',
  641.             'volume',
  642.             'lowLatencyModeEnabled',// Low Latency
  643.             'persistenceEnabled',// Mini Player
  644.         ];
  645.         var cachedValues = new Map();
  646.         for (var i = 0; i < keysToCache.length; i++) {
  647.             cachedValues.set(keysToCache[i], localStorage.getItem(keysToCache[i]));
  648.         }
  649.         var realSetItem = localStorage.setItem;
  650.         localStorage.setItem = function(key, value) {
  651.             if (cachedValues.has(key)) {
  652.                 cachedValues.set(key, value);
  653.             }
  654.             realSetItem.apply(this, arguments);
  655.         };
  656.         var realGetItem = localStorage.getItem;
  657.         localStorage.getItem = function(key) {
  658.             if (cachedValues.has(key)) {
  659.                 return cachedValues.get(key);
  660.             }
  661.             return realGetItem.apply(this, arguments);
  662.         };
  663.     }
  664.     if (window.Worker.toString().includes('twitch')) {
  665.         console.log('Twitch Worker is already hooked');
  666.     } else {
  667.         window.reloadTwitchPlayer = reloadTwitchPlayer;
  668.         declareOptions(window);
  669.         hookWindowWorker();
  670.         hookFetch();
  671.         if (document.readyState === "complete" || document.readyState === "loaded" || document.readyState === "interactive") {
  672.             onContentLoaded();
  673.         } else {
  674.             window.addEventListener("DOMContentLoaded", function() {
  675.                 onContentLoaded();
  676.             });
  677.         }
  678.     }
  679. })();
Add Comment
Please, Sign In to add comment