_dk

Untitled

_dk
Jun 17th, 2024
78
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 44.63 KB | None | 0 0
  1. // ==UserScript==
  2. // @name TwitchAdSolutions (vaft) (gloft)
  3. // @namespace https://github.com/GloftOfficial/TwitchAdSolutions
  4. // @version 6.1.0
  5. // @description Multiple solutions for blocking Twitch ads (vaft)
  6. // @updateURL https://github.com/GloftOfficial/TwitchAdSolutions/raw/master/vaft/vaft.user.js
  7. // @downloadURL https://github.com/GloftOfficial/TwitchAdSolutions/raw/master/vaft/vaft.user.js
  8. // @author https://github.com/cleanlock/VideoAdBlockForTwitch#credits
  9. // @match *://*.twitch.tv/*
  10. // @run-at document-start
  11. // @grant none
  12. // ==/UserScript==
  13. (function() {
  14. 'use strict';
  15. //This stops Twitch from pausing the player when in another tab and an ad shows.
  16. try {
  17. Object.defineProperty(document, 'visibilityState', {
  18. get() {
  19. return 'visible';
  20. }
  21. });
  22. Object.defineProperty(document, 'hidden', {
  23. get() {
  24. return false;
  25. }
  26. });
  27. const block = e => {
  28. e.preventDefault();
  29. e.stopPropagation();
  30. e.stopImmediatePropagation();
  31. };
  32. const process = e => {
  33. e.preventDefault();
  34. e.stopPropagation();
  35. e.stopImmediatePropagation();
  36. //This corrects the background tab buffer bug when switching to the background tab for the first time after an extended period.
  37. doTwitchPlayerTask(false, false, true, false, false);
  38. };
  39. document.addEventListener('visibilitychange', block, true);
  40. document.addEventListener('webkitvisibilitychange', block, true);
  41. document.addEventListener('mozvisibilitychange', block, true);
  42. document.addEventListener('hasFocus', block, true);
  43. if (/Firefox/.test(navigator.userAgent)) {
  44. Object.defineProperty(document, 'mozHidden', {
  45. get() {
  46. return false;
  47. }
  48. });
  49. } else {
  50. Object.defineProperty(document, 'webkitHidden', {
  51. get() {
  52. return false;
  53. }
  54. });
  55. }
  56. } catch (err) {}
  57. function declareOptions(scope) {
  58. scope.AdSignifier = 'stitched';
  59. scope.ClientID = 'kimne78kx3ncx6brgo4mv6wki5h1ko';
  60. scope.ClientVersion = 'null';
  61. scope.ClientSession = 'null';
  62. scope.PlayerType2 = 'proxy'; //Source
  63. scope.PlayerType3 = 'embed'; //Source
  64. scope.PlayerType4 = 'autoplay'; //360p
  65. scope.CurrentChannelName = null;
  66. scope.UsherParams = null;
  67. scope.WasShowingAd = false;
  68. scope.GQLDeviceID = null;
  69. scope.IsSquadStream = false;
  70. scope.StreamInfos = [];
  71. scope.StreamInfosByUrl = [];
  72. scope.MainUrlByUrl = [];
  73. scope.EncodingCacheTimeout = 60000;
  74. scope.ClientIntegrityHeader = null;
  75. scope.AuthorizationHeader = null;
  76. }
  77. declareOptions(window);
  78. var twitchWorkers = [];
  79. var adBlockDiv = null;
  80. var OriginalVideoPlayerQuality = null;
  81. var IsPlayerAutoQuality = null;
  82. const oldWorker = window.Worker;
  83. window.Worker = class Worker extends oldWorker {
  84. constructor(twitchBlobUrl) {
  85. var jsURL = getWasmWorkerUrl(twitchBlobUrl);
  86. if (typeof jsURL !== 'string') {
  87. super(twitchBlobUrl);
  88. return;
  89. }
  90. var newBlobStr = `
  91. ${getStreamUrlForResolution.toString()}
  92. ${getStreamForResolution.toString()}
  93. ${stripUnusedParams.toString()}
  94. ${processM3U8.toString()}
  95. ${hookWorkerFetch.toString()}
  96. ${declareOptions.toString()}
  97. ${getAccessToken.toString()}
  98. ${gqlRequest.toString()}
  99. ${adRecordgqlPacket.toString()}
  100. ${tryNotifyTwitch.toString()}
  101. ${parseAttributes.toString()}
  102. declareOptions(self);
  103. self.addEventListener('message', function(e) {
  104. if (e.data.key == 'UpdateIsSquadStream') {
  105. IsSquadStream = e.data.value;
  106. } else if (e.data.key == 'UpdateClientVersion') {
  107. ClientVersion = e.data.value;
  108. } else if (e.data.key == 'UpdateClientSession') {
  109. ClientSession = e.data.value;
  110. } else if (e.data.key == 'UpdateClientId') {
  111. ClientID = e.data.value;
  112. } else if (e.data.key == 'UpdateDeviceId') {
  113. GQLDeviceID = e.data.value;
  114. } else if (e.data.key == 'UpdateClientIntegrityHeader') {
  115. ClientIntegrityHeader = e.data.value;
  116. } else if (e.data.key == 'UpdateAuthorizationHeader') {
  117. AuthorizationHeader = e.data.value;
  118. }
  119. });
  120. hookWorkerFetch();
  121. importScripts('${jsURL}');
  122. `;
  123. super(URL.createObjectURL(new Blob([newBlobStr])));
  124. twitchWorkers.push(this);
  125. this.onmessage = function(e) {
  126. if (e.data.key == 'ShowAdBlockBanner') {
  127. if (adBlockDiv == null) {
  128. adBlockDiv = getAdBlockDiv();
  129. }
  130. adBlockDiv.P.textContent = 'Blocking ads';
  131. adBlockDiv.style.display = 'block';
  132. } else if (e.data.key == 'HideAdBlockBanner') {
  133. if (adBlockDiv == null) {
  134. adBlockDiv = getAdBlockDiv();
  135. }
  136. adBlockDiv.style.display = 'none';
  137. } else if (e.data.key == 'PauseResumePlayer') {
  138. doTwitchPlayerTask(true, false, false, false, false);
  139. } else if (e.data.key == 'ForceChangeQuality') {
  140. //This is used to fix the bug where the video would freeze.
  141. try {
  142. //if (navigator.userAgent.toLowerCase().indexOf('firefox') == -1) {
  143. return;
  144. //}
  145. var autoQuality = doTwitchPlayerTask(false, false, false, true, false);
  146. var currentQuality = doTwitchPlayerTask(false, true, false, false, false);
  147. if (IsPlayerAutoQuality == null) {
  148. IsPlayerAutoQuality = autoQuality;
  149. }
  150. if (OriginalVideoPlayerQuality == null) {
  151. OriginalVideoPlayerQuality = currentQuality;
  152. }
  153. if (!currentQuality.includes('360') || e.data.value != null) {
  154. if (!OriginalVideoPlayerQuality.includes('360')) {
  155. var settingsMenu = document.querySelector('div[data-a-target="player-settings-menu"]');
  156. if (settingsMenu == null) {
  157. var settingsCog = document.querySelector('button[data-a-target="player-settings-button"]');
  158. if (settingsCog) {
  159. settingsCog.click();
  160. var qualityMenu = document.querySelector('button[data-a-target="player-settings-menu-item-quality"]');
  161. if (qualityMenu) {
  162. qualityMenu.click();
  163. }
  164. var lowQuality = document.querySelectorAll('input[data-a-target="tw-radio"');
  165. if (lowQuality) {
  166. var qualityToSelect = lowQuality.length - 2;
  167. if (e.data.value != null) {
  168. if (e.data.value.includes('original')) {
  169. e.data.value = OriginalVideoPlayerQuality;
  170. if (IsPlayerAutoQuality) {
  171. e.data.value = 'auto';
  172. }
  173. }
  174. if (e.data.value.includes('160p')) {
  175. qualityToSelect = 5;
  176. }
  177. if (e.data.value.includes('360p')) {
  178. qualityToSelect = 4;
  179. }
  180. if (e.data.value.includes('480p')) {
  181. qualityToSelect = 3;
  182. }
  183. if (e.data.value.includes('720p')) {
  184. qualityToSelect = 2;
  185. }
  186. if (e.data.value.includes('822p')) {
  187. qualityToSelect = 2;
  188. }
  189. if (e.data.value.includes('864p')) {
  190. qualityToSelect = 2;
  191. }
  192. if (e.data.value.includes('900p')) {
  193. qualityToSelect = 2;
  194. }
  195. if (e.data.value.includes('936p')) {
  196. qualityToSelect = 2;
  197. }
  198. if (e.data.value.includes('960p')) {
  199. qualityToSelect = 2;
  200. }
  201. if (e.data.value.includes('1080p')) {
  202. qualityToSelect = 2;
  203. }
  204. if (e.data.value.includes('source')) {
  205. qualityToSelect = 1;
  206. }
  207. if (e.data.value.includes('auto')) {
  208. qualityToSelect = 0;
  209. }
  210. }
  211. var currentQualityLS = window.localStorage.getItem('video-quality');
  212. lowQuality[qualityToSelect].click();
  213. settingsCog.click();
  214. window.localStorage.setItem('video-quality', currentQualityLS);
  215. if (e.data.value != null) {
  216. OriginalVideoPlayerQuality = null;
  217. IsPlayerAutoQuality = null;
  218. doTwitchPlayerTask(false, false, false, true, true);
  219. }
  220. }
  221. }
  222. }
  223. }
  224. }
  225. } catch (err) {
  226. OriginalVideoPlayerQuality = null;
  227. IsPlayerAutoQuality = null;
  228. }
  229. }
  230. };
  231. function getAdBlockDiv() {
  232. //To display a notification to the user, that an ad is being blocked.
  233. var playerRootDiv = document.querySelector('.video-player');
  234. var adBlockDiv = null;
  235. if (playerRootDiv != null) {
  236. adBlockDiv = playerRootDiv.querySelector('.adblock-overlay');
  237. if (adBlockDiv == null) {
  238. adBlockDiv = document.createElement('div');
  239. adBlockDiv.className = 'adblock-overlay';
  240. 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>';
  241. adBlockDiv.style.display = 'none';
  242. adBlockDiv.P = adBlockDiv.querySelector('p');
  243. playerRootDiv.appendChild(adBlockDiv);
  244. }
  245. }
  246. return adBlockDiv;
  247. }
  248. }
  249. };
  250. function getWasmWorkerUrl(twitchBlobUrl) {
  251. var req = new XMLHttpRequest();
  252. req.open('GET', twitchBlobUrl, false);
  253. req.send();
  254. return req.responseText.split("'")[1];
  255. }
  256. function hookWorkerFetch() {
  257. console.log('hookWorkerFetch');
  258. var realFetch = fetch;
  259. fetch = async function(url, options) {
  260. if (typeof url === 'string') {
  261. if (url.endsWith('m3u8')) {
  262. return new Promise(function(resolve, reject) {
  263. var processAfter = async function(response) {
  264. //Here we check the m3u8 for any ads and also try fallback player types if needed.
  265. var responseText = await response.text();
  266. var weaverText = null;
  267. weaverText = await processM3U8(url, responseText, realFetch, PlayerType2);
  268. if (weaverText.includes(AdSignifier)) {
  269. weaverText = await processM3U8(url, responseText, realFetch, PlayerType3);
  270. }
  271. if (weaverText.includes(AdSignifier)) {
  272. weaverText = await processM3U8(url, responseText, realFetch, PlayerType4);
  273. }
  274. resolve(new Response(weaverText));
  275. };
  276. var send = function() {
  277. return realFetch(url, options).then(function(response) {
  278. processAfter(response);
  279. })['catch'](function(err) {
  280. reject(err);
  281. });
  282. };
  283. send();
  284. });
  285. } else if (url.includes('/api/channel/hls/')) {
  286. var channelName = (new URL(url)).pathname.match(/([^\/]+)(?=\.\w+$)/)[0];
  287. UsherParams = (new URL(url)).search;
  288. CurrentChannelName = channelName;
  289. //To prevent pause/resume loop for mid-rolls.
  290. var isPBYPRequest = url.includes('picture-by-picture');
  291. if (isPBYPRequest) {
  292. url = '';
  293. }
  294. return new Promise(function(resolve, reject) {
  295. var processAfter = async function(response) {
  296. if (response.status == 200) {
  297. encodingsM3u8 = await response.text();
  298. var streamInfo = StreamInfos[channelName];
  299. if (streamInfo == null) {
  300. StreamInfos[channelName] = streamInfo = {};
  301. }
  302. streamInfo.ChannelName = channelName;
  303. streamInfo.RequestedAds = new Set();
  304. streamInfo.Urls = [];// xxx.m3u8 -> { Resolution: "284x160", FrameRate: 30.0 }
  305. streamInfo.EncodingsM3U8Cache = [];
  306. streamInfo.EncodingsM3U8 = encodingsM3u8;
  307. var lines = encodingsM3u8.replace('\r', '').split('\n');
  308. for (var i = 0; i < lines.length; i++) {
  309. if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) {
  310. streamInfo.Urls[lines[i]] = -1;
  311. if (i > 0 && lines[i - 1].startsWith('#EXT-X-STREAM-INF')) {
  312. var attributes = parseAttributes(lines[i - 1]);
  313. var resolution = attributes['RESOLUTION'];
  314. var frameRate = attributes['FRAME-RATE'];
  315. if (resolution) {
  316. streamInfo.Urls[lines[i]] = {
  317. Resolution: resolution,
  318. FrameRate: frameRate
  319. };
  320. }
  321. }
  322. StreamInfosByUrl[lines[i]] = streamInfo;
  323. MainUrlByUrl[lines[i]] = url;
  324. }
  325. }
  326. resolve(new Response(encodingsM3u8));
  327. } else {
  328. resolve(response);
  329. }
  330. };
  331. var send = function() {
  332. return realFetch(url, options).then(function(response) {
  333. processAfter(response);
  334. })['catch'](function(err) {
  335. reject(err);
  336. });
  337. };
  338. send();
  339. });
  340. }
  341. }
  342. return realFetch.apply(this, arguments);
  343. };
  344. }
  345. function getStreamUrlForResolution(encodingsM3u8, resolutionInfo, qualityOverrideStr) {
  346. var qualityOverride = 0;
  347. if (qualityOverrideStr && qualityOverrideStr.endsWith('p')) {
  348. qualityOverride = qualityOverrideStr.substr(0, qualityOverrideStr.length - 1) | 0;
  349. }
  350. var qualityOverrideFoundQuality = 0;
  351. var qualityOverrideFoundFrameRate = 0;
  352. var encodingsLines = encodingsM3u8.replace('\r', '').split('\n');
  353. var firstUrl = null;
  354. var lastUrl = null;
  355. var matchedResolutionUrl = null;
  356. var matchedFrameRate = false;
  357. for (var i = 0; i < encodingsLines.length; i++) {
  358. if (!encodingsLines[i].startsWith('#') && encodingsLines[i].includes('.m3u8')) {
  359. if (i > 0 && encodingsLines[i - 1].startsWith('#EXT-X-STREAM-INF')) {
  360. var attributes = parseAttributes(encodingsLines[i - 1]);
  361. var resolution = attributes['RESOLUTION'];
  362. var frameRate = attributes['FRAME-RATE'];
  363. if (resolution) {
  364. if (qualityOverride) {
  365. var quality = resolution.toLowerCase().split('x')[1];
  366. if (quality == qualityOverride) {
  367. qualityOverrideFoundQuality = quality;
  368. qualityOverrideFoundFrameRate = frameRate;
  369. matchedResolutionUrl = encodingsLines[i];
  370. if (frameRate < 40) {
  371. //console.log(`qualityOverride(A) quality:${quality} frameRate:${frameRate}`);
  372. return matchedResolutionUrl;
  373. }
  374. } else if (quality < qualityOverride) {
  375. //if (matchedResolutionUrl) {
  376. // console.log(`qualityOverride(B) quality:${qualityOverrideFoundQuality} frameRate:${qualityOverrideFoundFrameRate}`);
  377. //} else {
  378. // console.log(`qualityOverride(C) quality:${quality} frameRate:${frameRate}`);
  379. //}
  380. return matchedResolutionUrl ? matchedResolutionUrl : encodingsLines[i];
  381. }
  382. } else if ((!resolutionInfo || resolution == resolutionInfo.Resolution) &&
  383. (!matchedResolutionUrl || (!matchedFrameRate && frameRate == resolutionInfo.FrameRate))) {
  384. matchedResolutionUrl = encodingsLines[i];
  385. matchedFrameRate = frameRate == resolutionInfo.FrameRate;
  386. if (matchedFrameRate) {
  387. return matchedResolutionUrl;
  388. }
  389. }
  390. }
  391. if (firstUrl == null) {
  392. firstUrl = encodingsLines[i];
  393. }
  394. lastUrl = encodingsLines[i];
  395. }
  396. }
  397. }
  398. if (qualityOverride) {
  399. return lastUrl;
  400. }
  401. return matchedResolutionUrl ? matchedResolutionUrl : firstUrl;
  402. }
  403. async function getStreamForResolution(streamInfo, resolutionInfo, encodingsM3u8, fallbackStreamStr, playerType, realFetch) {
  404. var qualityOverride = null;
  405. if (streamInfo.EncodingsM3U8Cache[playerType].Resolution != resolutionInfo.Resolution ||
  406. streamInfo.EncodingsM3U8Cache[playerType].RequestTime < Date.now() - EncodingCacheTimeout) {
  407. console.log(`Blocking ads (type:${playerType}, resolution:${resolutionInfo.Resolution}, frameRate:${resolutionInfo.FrameRate}, qualityOverride:${qualityOverride})`);
  408. }
  409. streamInfo.EncodingsM3U8Cache[playerType].RequestTime = Date.now();
  410. streamInfo.EncodingsM3U8Cache[playerType].Value = encodingsM3u8;
  411. streamInfo.EncodingsM3U8Cache[playerType].Resolution = resolutionInfo.Resolution;
  412. var streamM3u8Url = getStreamUrlForResolution(encodingsM3u8, resolutionInfo, qualityOverride);
  413. var streamM3u8Response = await realFetch(streamM3u8Url);
  414. if (streamM3u8Response.status == 200) {
  415. var m3u8Text = await streamM3u8Response.text();
  416. WasShowingAd = true;
  417. postMessage({
  418. key: 'ShowAdBlockBanner'
  419. });
  420. postMessage({
  421. key: 'ForceChangeQuality'
  422. });
  423. if (!m3u8Text || m3u8Text.includes(AdSignifier)) {
  424. streamInfo.EncodingsM3U8Cache[playerType].Value = null;
  425. }
  426. return m3u8Text;
  427. } else {
  428. streamInfo.EncodingsM3U8Cache[playerType].Value = null;
  429. return fallbackStreamStr;
  430. }
  431. }
  432. function stripUnusedParams(str, params) {
  433. if (!params) {
  434. params = [ 'token', 'sig' ];
  435. }
  436. var tempUrl = new URL('https://localhost/' + str);
  437. for (var i = 0; i < params.length; i++) {
  438. tempUrl.searchParams.delete(params[i]);
  439. }
  440. return tempUrl.pathname.substring(1) + tempUrl.search;
  441. }
  442. async function processM3U8(url, textStr, realFetch, playerType) {
  443. //Checks the m3u8 for ads and if it finds one, instead returns an ad-free stream.
  444. var streamInfo = StreamInfosByUrl[url];
  445. //Ad blocking for squad streams is disabled due to the way multiple weaver urls are used. No workaround so far.
  446. if (IsSquadStream == true) {
  447. return textStr;
  448. }
  449. if (!textStr) {
  450. return textStr;
  451. }
  452. //Some live streams use mp4.
  453. if (!textStr.includes('.ts') && !textStr.includes('.mp4')) {
  454. return textStr;
  455. }
  456. var haveAdTags = textStr.includes(AdSignifier);
  457. if (haveAdTags) {
  458. var isMidroll = textStr.includes('"MIDROLL"') || textStr.includes('"midroll"');
  459. //Reduces ad frequency. TODO: Reduce the number of requests. This is really spamming Twitch with requests.
  460. if (!isMidroll) {
  461. if (playerType === PlayerType2) {
  462. var lines = textStr.replace('\r', '').split('\n');
  463. for (var i = 0; i < lines.length; i++) {
  464. var line = lines[i];
  465. if (line.startsWith('#EXTINF') && lines.length > i + 1) {
  466. if (!line.includes(',live') && !streamInfo.RequestedAds.has(lines[i + 1])) {
  467. // Only request one .ts file per .m3u8 request to avoid making too many requests
  468. //console.log('Fetch ad .ts file');
  469. streamInfo.RequestedAds.add(lines[i + 1]);
  470. fetch(lines[i + 1]).then((response)=>{response.blob()});
  471. break;
  472. }
  473. }
  474. }
  475. }
  476. try {
  477. //tryNotifyTwitch(textStr);
  478. } catch (err) {}
  479. }
  480. var currentResolution = null;
  481. if (streamInfo && streamInfo.Urls) {
  482. for (const [resUrl, resInfo] of Object.entries(streamInfo.Urls)) {
  483. if (resUrl == url) {
  484. currentResolution = resInfo;
  485. //console.log(resInfo.Resolution);
  486. break;
  487. }
  488. }
  489. }
  490. // Keep the m3u8 around for a little while (once per ad) before requesting a new one
  491. var encodingsM3U8Cache = streamInfo.EncodingsM3U8Cache[playerType];
  492. if (encodingsM3U8Cache) {
  493. if (encodingsM3U8Cache.Value && encodingsM3U8Cache.RequestTime >= Date.now() - EncodingCacheTimeout) {
  494. try {
  495. var result = getStreamForResolution(streamInfo, currentResolution, encodingsM3U8Cache.Value, null, playerType, realFetch);
  496. if (result) {
  497. return result;
  498. }
  499. } catch (err) {
  500. encodingsM3U8Cache.Value = null;
  501. }
  502. }
  503. } else {
  504. streamInfo.EncodingsM3U8Cache[playerType] = {
  505. RequestTime: Date.now(),
  506. Value: null,
  507. Resolution: null
  508. };
  509. }
  510. if (playerType === 'proxy') {
  511. try {
  512. //var proxyType = TwitchAdblockSettings.ProxyType ? TwitchAdblockSettings.ProxyType : DefaultProxyType;
  513. var encodingsM3u8Response = null;
  514. try{
  515. encodingsM3u8Response = await realFetch(String(atob(atob('YUhSMGNITTZMeTloWkdKc2IyTnJMbWRzYjJaMExtMWxMejlqYUdGdVBRPT0=')) + CurrentChannelName), {method: 'GET'});
  516. }catch(ttt){}
  517. if (encodingsM3u8Response && encodingsM3u8Response.status === 200) {
  518. return getStreamForResolution(streamInfo, currentResolution, await encodingsM3u8Response.text(), textStr, playerType, realFetch);
  519. }
  520. }catch (err) {console.log(err);}
  521. return textStr;
  522. }
  523. var accessTokenResponse = await getAccessToken(CurrentChannelName, playerType);
  524. if (accessTokenResponse.status === 200) {
  525. var accessToken = await accessTokenResponse.json();
  526. try {
  527. var urlInfo = new URL('https://usher.ttvnw.net/api/channel/hls/' + CurrentChannelName + '.m3u8' + UsherParams);
  528. urlInfo.searchParams.set('sig', accessToken.data.streamPlaybackAccessToken.signature);
  529. urlInfo.searchParams.set('token', accessToken.data.streamPlaybackAccessToken.value);
  530. var encodingsM3u8Response = await realFetch(urlInfo.href);
  531. if (encodingsM3u8Response.status === 200) {
  532. return getStreamForResolution(streamInfo, currentResolution, await encodingsM3u8Response.text(), textStr, playerType, realFetch);
  533. } else {
  534. return textStr;
  535. }
  536. } catch (err) {}
  537. return textStr;
  538. } else {
  539. return textStr;
  540. }
  541. } else {
  542. if (WasShowingAd) {
  543. console.log('Finished blocking ads');
  544. WasShowingAd = false;
  545. //Here we put player back to original quality and remove the blocking message.
  546. postMessage({
  547. key: 'ForceChangeQuality',
  548. value: 'original'
  549. });
  550. postMessage({
  551. key: 'PauseResumePlayer'
  552. });
  553. postMessage({
  554. key: 'HideAdBlockBanner'
  555. });
  556. }
  557. return textStr;
  558. }
  559. return textStr;
  560. }
  561. function parseAttributes(str) {
  562. return Object.fromEntries(
  563. str.split(/(?:^|,)((?:[^=]*)=(?:"[^"]*"|[^,]*))/)
  564. .filter(Boolean)
  565. .map(x => {
  566. const idx = x.indexOf('=');
  567. const key = x.substring(0, idx);
  568. const value = x.substring(idx + 1);
  569. const num = Number(value);
  570. return [key, Number.isNaN(num) ? value.startsWith('"') ? JSON.parse(value) : value : num];
  571. }));
  572. }
  573. async function tryNotifyTwitch(streamM3u8) {
  574. //We notify that an ad was requested but was not visible and was also muted.
  575. var matches = streamM3u8.match(/#EXT-X-DATERANGE:(ID="stitched-ad-[^\n]+)\n/);
  576. if (matches.length > 1) {
  577. const attrString = matches[1];
  578. const attr = parseAttributes(attrString);
  579. var podLength = parseInt(attr['X-TV-TWITCH-AD-POD-LENGTH'] ? attr['X-TV-TWITCH-AD-POD-LENGTH'] : '1');
  580. var podPosition = parseInt(attr['X-TV-TWITCH-AD-POD-POSITION'] ? attr['X-TV-TWITCH-AD-POD-POSITION'] : '0');
  581. var radToken = attr['X-TV-TWITCH-AD-RADS-TOKEN'];
  582. var lineItemId = attr['X-TV-TWITCH-AD-LINE-ITEM-ID'];
  583. var orderId = attr['X-TV-TWITCH-AD-ORDER-ID'];
  584. var creativeId = attr['X-TV-TWITCH-AD-CREATIVE-ID'];
  585. var adId = attr['X-TV-TWITCH-AD-ADVERTISER-ID'];
  586. var rollType = attr['X-TV-TWITCH-AD-ROLL-TYPE'].toLowerCase();
  587. const baseData = {
  588. stitched: true,
  589. roll_type: rollType,
  590. player_mute: true,
  591. player_volume: 0.0,
  592. visible: false,
  593. };
  594. for (let podPosition = 0; podPosition < podLength; podPosition++) {
  595. const extendedData = {
  596. ...baseData,
  597. ad_id: adId,
  598. ad_position: podPosition,
  599. duration: 0,
  600. creative_id: creativeId,
  601. total_ads: podLength,
  602. order_id: orderId,
  603. line_item_id: lineItemId,
  604. };
  605. await gqlRequest(adRecordgqlPacket('video_ad_impression', radToken, extendedData));
  606. for (let quartile = 0; quartile < 4; quartile++) {
  607. await gqlRequest(
  608. adRecordgqlPacket('video_ad_quartile_complete', radToken, {
  609. ...extendedData,
  610. quartile: quartile + 1,
  611. })
  612. );
  613. }
  614. await gqlRequest(adRecordgqlPacket('video_ad_pod_complete', radToken, baseData));
  615. }
  616. }
  617. }
  618. function adRecordgqlPacket(event, radToken, payload) {
  619. return [{
  620. operationName: 'ClientSideAdEventHandling_RecordAdEvent',
  621. variables: {
  622. input: {
  623. eventName: event,
  624. eventPayload: JSON.stringify(payload),
  625. radToken,
  626. },
  627. },
  628. extensions: {
  629. persistedQuery: {
  630. version: 1,
  631. sha256Hash: '7e6c69e6eb59f8ccb97ab73686f3d8b7d85a72a0298745ccd8bfc68e4054ca5b',
  632. },
  633. },
  634. }];
  635. }
  636. function getAccessToken(channelName, playerType, realFetch) {
  637. var body = null;
  638. var templateQuery = 'query PlaybackAccessToken_Template($login: String!, $isLive: Boolean!, $vodID: ID!, $isVod: Boolean!, $playerType: String!) { streamPlaybackAccessToken(channelName: $login, params: {platform: "ios", playerBackend: "mediaplayer", playerType: $playerType}) @include(if: $isLive) { value signature __typename } videoPlaybackAccessToken(id: $vodID, params: {platform: "ios", playerBackend: "mediaplayer", playerType: $playerType}) @include(if: $isVod) { value signature __typename }}';
  639. body = {
  640. operationName: 'PlaybackAccessToken_Template',
  641. query: templateQuery,
  642. variables: {
  643. 'isLive': true,
  644. 'login': channelName,
  645. 'isVod': false,
  646. 'vodID': '',
  647. 'playerType': playerType
  648. }
  649. };
  650. return gqlRequest(body, realFetch);
  651. }
  652. function gqlRequest(body, realFetch) {
  653. if (ClientIntegrityHeader == null) {
  654. //console.warn('ClientIntegrityHeader is null');
  655. //throw 'ClientIntegrityHeader is null';
  656. }
  657. var fetchFunc = realFetch ? realFetch : fetch;
  658. if (!GQLDeviceID) {
  659. var dcharacters = 'abcdefghijklmnopqrstuvwxyz0123456789';
  660. var dcharactersLength = dcharacters.length;
  661. for (var i = 0; i < 32; i++) {
  662. GQLDeviceID += dcharacters.charAt(Math.floor(Math.random() * dcharactersLength));
  663. }
  664. }
  665. return fetchFunc('https://gql.twitch.tv/gql', {
  666. method: 'POST',
  667. body: JSON.stringify(body),
  668. headers: {
  669. 'Client-ID': ClientID,
  670. 'Client-Integrity': ClientIntegrityHeader,
  671. 'Device-ID': GQLDeviceID,
  672. 'X-Device-Id': GQLDeviceID,
  673. 'Client-Version': ClientVersion,
  674. 'Client-Session-Id': ClientSession,
  675. 'Authorization': AuthorizationHeader
  676. }
  677. });
  678. }
  679. function doTwitchPlayerTask(isPausePlay, isCheckQuality, isCorrectBuffer, isAutoQuality, setAutoQuality) {
  680. //This will do an instant pause/play to return to original quality once the ad is finished.
  681. //We also use this function to get the current video player quality set by the user.
  682. //We also use this function to quickly pause/play the player when switching tabs to stop delays.
  683. try {
  684. var videoController = null;
  685. var videoPlayer = null;
  686. function findReactNode(root, constraint) {
  687. if (root.stateNode && constraint(root.stateNode)) {
  688. return root.stateNode;
  689. }
  690. let node = root.child;
  691. while (node) {
  692. const result = findReactNode(node, constraint);
  693. if (result) {
  694. return result;
  695. }
  696. node = node.sibling;
  697. }
  698. return null;
  699. }
  700. function findReactRootNode() {
  701. var reactRootNode = null;
  702. var rootNode = document.querySelector('#root');
  703. if (rootNode && rootNode._reactRootContainer && rootNode._reactRootContainer._internalRoot && rootNode._reactRootContainer._internalRoot.current) {
  704. reactRootNode = rootNode._reactRootContainer._internalRoot.current;
  705. }
  706. if (reactRootNode == null) {
  707. var containerName = Object.keys(rootNode).find(x => x.startsWith('__reactContainer'));
  708. if (containerName != null) {
  709. reactRootNode = rootNode[containerName];
  710. }
  711. }
  712. return reactRootNode;
  713. }
  714. var reactRootNode = findReactRootNode();
  715. if (!reactRootNode) {
  716. console.log('Could not find react root');
  717. return;
  718. }
  719. videoPlayer = findReactNode(reactRootNode, node => node.setPlayerActive && node.props && node.props.mediaPlayerInstance);
  720. videoPlayer = videoPlayer && videoPlayer.props && videoPlayer.props.mediaPlayerInstance ? videoPlayer.props.mediaPlayerInstance : null;
  721. if (isPausePlay) {
  722. videoPlayer.pause();
  723. videoPlayer.play();
  724. return;
  725. }
  726. if (isCheckQuality) {
  727. if (typeof videoPlayer.getQuality() == 'undefined') {
  728. return;
  729. }
  730. var playerQuality = JSON.stringify(videoPlayer.getQuality());
  731. if (playerQuality) {
  732. return playerQuality;
  733. } else {
  734. return;
  735. }
  736. }
  737. if (isAutoQuality) {
  738. if (typeof videoPlayer.isAutoQualityMode() == 'undefined') {
  739. return false;
  740. }
  741. var autoQuality = videoPlayer.isAutoQualityMode();
  742. if (autoQuality) {
  743. videoPlayer.setAutoQualityMode(false);
  744. return autoQuality;
  745. } else {
  746. return false;
  747. }
  748. }
  749. if (setAutoQuality) {
  750. videoPlayer.setAutoQualityMode(true);
  751. return;
  752. }
  753. //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.
  754. //We check that this is a live stream by the page URL, to prevent vod/clip pause/plays.
  755. try {
  756. var currentPageURL = document.URL;
  757. var isLive = true;
  758. if (currentPageURL.includes('videos/') || currentPageURL.includes('clip/')) {
  759. isLive = false;
  760. }
  761. if (isCorrectBuffer && isLive) {
  762. //A timer is needed due to the player not resuming without it.
  763. setTimeout(function() {
  764. //If latency to broadcaster is above 5 or 15 seconds upon switching tabs, we pause and play the player to reset the latency.
  765. //If latency is between 0-6, user can manually pause and resume to reset latency further.
  766. if (videoPlayer.isLiveLowLatency() && videoPlayer.getLiveLatency() > 5) {
  767. videoPlayer.pause();
  768. videoPlayer.play();
  769. } else if (videoPlayer.getLiveLatency() > 15) {
  770. videoPlayer.pause();
  771. videoPlayer.play();
  772. }
  773. }, 3000);
  774. }
  775. } catch (err) {}
  776. } catch (err) {}
  777. }
  778. window.reloadTwitchPlayer = doTwitchPlayerTask;
  779. var localDeviceID = null;
  780. localDeviceID = window.localStorage.getItem('local_copy_unique_id');
  781. function postTwitchWorkerMessage(key, value) {
  782. twitchWorkers.forEach((worker) => {
  783. worker.postMessage({key: key, value: value});
  784. });
  785. }
  786. function hookFetch() {
  787. var realFetch = window.fetch;
  788. window.fetch = function(url, init, ...args) {
  789. if (typeof url === 'string') {
  790. //Check if squad stream.
  791. if (window.location.pathname.includes('/squad')) {
  792. postTwitchWorkerMessage('UpdateIsSquadStream', true);
  793. } else {
  794. postTwitchWorkerMessage('UpdateIsSquadStream', false);
  795. }
  796. if (url.includes('/access_token') || url.includes('gql')) {
  797. //Device ID is used when notifying Twitch of ads.
  798. var deviceId = init.headers['X-Device-Id'];
  799. if (typeof deviceId !== 'string') {
  800. deviceId = init.headers['Device-ID'];
  801. }
  802. //Added to prevent eventual UBlock conflicts.
  803. if (typeof deviceId === 'string' && !deviceId.includes('twitch-web-wall-mason')) {
  804. GQLDeviceID = deviceId;
  805. } else if (localDeviceID) {
  806. GQLDeviceID = localDeviceID.replace('"', '');
  807. GQLDeviceID = GQLDeviceID.replace('"', '');
  808. }
  809. if (GQLDeviceID) {
  810. if (typeof init.headers['X-Device-Id'] === 'string') {
  811. init.headers['X-Device-Id'] = GQLDeviceID;
  812. }
  813. if (typeof init.headers['Device-ID'] === 'string') {
  814. init.headers['Device-ID'] = GQLDeviceID;
  815. }
  816. postTwitchWorkerMessage('UpdateDeviceId', GQLDeviceID);
  817. }
  818. //Client version is used in GQL requests.
  819. var clientVersion = init.headers['Client-Version'];
  820. if (clientVersion && typeof clientVersion == 'string') {
  821. ClientVersion = clientVersion;
  822. }
  823. if (ClientVersion) {
  824. postTwitchWorkerMessage('UpdateClientVersion', ClientVersion);
  825. }
  826. //Client session is used in GQL requests.
  827. var clientSession = init.headers['Client-Session-Id'];
  828. if (clientSession && typeof clientSession == 'string') {
  829. ClientSession = clientSession;
  830. }
  831. if (ClientSession) {
  832. postTwitchWorkerMessage('UpdateClientSession', ClientSession);
  833. }
  834. //Client ID is used in GQL requests.
  835. if (url.includes('gql') && init && typeof init.body === 'string' && init.body.includes('PlaybackAccessToken')) {
  836. var clientId = init.headers['Client-ID'];
  837. if (clientId && typeof clientId == 'string') {
  838. ClientID = clientId;
  839. } else {
  840. clientId = init.headers['Client-Id'];
  841. if (clientId && typeof clientId == 'string') {
  842. ClientID = clientId;
  843. }
  844. }
  845. if (ClientID) {
  846. postTwitchWorkerMessage('UpdateClientId', ClientID);
  847. }
  848. //Client integrity header
  849. ClientIntegrityHeader = init.headers['Client-Integrity'];
  850. if (ClientIntegrityHeader) {
  851. postTwitchWorkerMessage('UpdateClientIntegrityHeader', ClientIntegrityHeader);
  852. }
  853. //Authorization header
  854. AuthorizationHeader = init.headers['Authorization'];
  855. if (AuthorizationHeader) {
  856. postTwitchWorkerMessage('UpdateAuthorizationHeader', AuthorizationHeader);
  857. }
  858. }
  859. //To prevent pause/resume loop for mid-rolls.
  860. if (url.includes('gql') && init && typeof init.body === 'string' && init.body.includes('PlaybackAccessToken') && init.body.includes('picture-by-picture')) {
  861. init.body = '';
  862. }
  863. var isPBYPRequest = url.includes('picture-by-picture');
  864. if (isPBYPRequest) {
  865. url = '';
  866. }
  867. }
  868. }
  869. return realFetch.apply(this, arguments);
  870. };
  871. }
  872. hookFetch();
  873. })();
Add Comment
Please, Sign In to add comment