Youtube HLS Enabler

Play the hls manifest from the ios player response. Based on https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass

  1. // ==UserScript==
  2. // @name Youtube HLS Enabler
  3. // @namespace https://github.com/pepeloni-away
  4. // @author pploni
  5. // @run-at document-start
  6. // @insert-into page
  7. // @version 1.81
  8. // @description Play the hls manifest from the ios player response. Based on https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass
  9. // @grant GM_xmlhttpRequest
  10. // @grant GM_registerMenuCommand
  11. // @grant GM_setClipboard
  12. // @require https://cdn.jsdelivr.net/npm/hls.js@1
  13. // @match https://www.youtube.com/*
  14. // ==/UserScript==
  15.  
  16. /* user options */
  17.  
  18. // show a toast notification when successfully obtaining the hls manifest
  19. const notifyOnSuccess = false
  20. // only fetch the hls manifest when premium 1080p is available
  21. // NOTE: youtube doesn't show the premium 1080p option in embeds or when the user is not logged in
  22. const onlyOnPremiumAvailable = true
  23. // automatically switch to the hls manifest when it is added to the player
  24. const onByDefault = false
  25. // show a toasat notification when 616 is in the hls manifest
  26. const notify616 = false
  27. // switch to the hls manifest when it contains 616
  28. const onBy616 = true
  29. const disableLogging = false
  30.  
  31. // what is 616? what do the changing numbers on the toggle button mean?
  32. // they are youtube specific format ids, ctrl-f them on https://gist.github.com/MartinEesmaa/2f4b261cb90a47e9c41ba115a011a4aa
  33.  
  34. /* end user options */
  35. const console = {
  36. log: disableLogging ? function () {} : unsafeWindow.console.log
  37. }
  38.  
  39. if (unsafeWindow.trustedTypes && unsafeWindow.trustedTypes.createPolicy) {
  40. if (!unsafeWindow.trustedTypes.defaultPolicy) {
  41. const fn = _ => _
  42. trustedTypes.createPolicy('default', {
  43. createHTML: fn,
  44. createScriptURL: fn,
  45. createScript: fn,
  46. })
  47. }
  48. else {
  49. console.log('there already is a default trustedtypes policy, should probably intercept it')
  50. }
  51. }
  52.  
  53.  
  54. const VALID_PLAYABILITY_STATUSES = ['OK', 'LIVE_STREAM_OFFLINE'];
  55. const GOOGLE_AUTH_HEADER_NAMES = [
  56. // 'Authorization',
  57. // 'X-Goog-AuthUser',
  58. // 'X-Origin',
  59. 'X-Goog-Visitor-Id',
  60. ];
  61.  
  62. var proxy = {
  63. getPlayer,
  64. getNext,
  65. getGoogleVideoUrl,
  66. };
  67.  
  68. let nextResponseCache = {};
  69.  
  70. function getGoogleVideoUrl(originalUrl) {
  71. return Config.VIDEO_PROXY_SERVER_HOST + '/direct/' + btoa(originalUrl.toString());
  72. }
  73.  
  74. function getPlayer(payload) {
  75. // Also request the /next response if a later /next request is likely.
  76. if (!nextResponseCache[payload.videoId] && !isMusic && !isEmbed) {
  77. payload.includeNext = 1;
  78. }
  79.  
  80. return sendRequest('getPlayer', payload);
  81. }
  82.  
  83. function getNext(payload) {
  84. // Next response already cached? => Return cached content
  85. if (nextResponseCache[payload.videoId]) {
  86. return nextResponseCache[payload.videoId];
  87. }
  88.  
  89. return sendRequest('getNext', payload);
  90. }
  91.  
  92. function sendRequest(endpoint, payload) {
  93. const queryParams = new URLSearchParams(payload);
  94. const proxyUrl = `${Config.ACCOUNT_PROXY_SERVER_HOST}/${endpoint}?${queryParams}&client=js`;
  95.  
  96. try {
  97. const xmlhttp = new XMLHttpRequest();
  98. xmlhttp.open('GET', proxyUrl, false);
  99. xmlhttp.send(null);
  100.  
  101. const proxyResponse = nativeJSONParse(xmlhttp.responseText);
  102.  
  103. // Mark request as 'proxied'
  104. proxyResponse.proxied = true;
  105.  
  106. // Put included /next response in the cache
  107. if (proxyResponse.nextResponse) {
  108. nextResponseCache[payload.videoId] = proxyResponse.nextResponse;
  109. delete proxyResponse.nextResponse;
  110. }
  111.  
  112. return proxyResponse;
  113. } catch (err) {
  114. console.log(err, 'Proxy API Error');
  115. return { errorMessage: 'Proxy Connection failed' };
  116. }
  117. }
  118.  
  119. var Config = window[Symbol()] = {
  120. // UNLOCKABLE_PLAYABILITY_STATUSES,
  121. VALID_PLAYABILITY_STATUSES,
  122. // ACCOUNT_PROXY_SERVER_HOST,
  123. // VIDEO_PROXY_SERVER_HOST,
  124. // ENABLE_UNLOCK_CONFIRMATION_EMBED,
  125. // ENABLE_UNLOCK_NOTIFICATION,
  126. // SKIP_CONTENT_WARNINGS,
  127. GOOGLE_AUTH_HEADER_NAMES,
  128. // BLURRED_THUMBNAIL_SQP_LENGTHS,
  129. };
  130.  
  131. var innertube = {
  132. getPlayer: getPlayer$1,
  133. getNext: getNext$1,
  134. };
  135.  
  136. function getPlayer$1(payload, useAuth) {
  137. return sendInnertubeRequest('v1/player', payload, useAuth);
  138. }
  139.  
  140. function getNext$1(payload, useAuth) {
  141. return sendInnertubeRequest('v1/next', payload, useAuth);
  142. }
  143.  
  144. function sendInnertubeRequest(endpoint, payload, useAuth) {
  145. const xmlhttp = new XMLHttpRequest();
  146. xmlhttp.open('POST', `/youtubei/${endpoint}?key=${getYtcfgValue('INNERTUBE_API_KEY')}&prettyPrint=false`, false);
  147.  
  148. if (useAuth /*&& isUserLoggedIn()*/) {
  149. xmlhttp.withCredentials = true;
  150. Config.GOOGLE_AUTH_HEADER_NAMES.forEach((headerName) => {
  151. xmlhttp.setRequestHeader(headerName, get(headerName));
  152. });
  153. }
  154.  
  155. xmlhttp.send(JSON.stringify(payload));
  156. return nativeJSONParse(xmlhttp.responseText);
  157. }
  158.  
  159. const localStoragePrefix = '1080pp_';
  160.  
  161. function set(key, value) {
  162. localStorage.setItem(localStoragePrefix + key, JSON.stringify(value));
  163. }
  164.  
  165. function get(key) {
  166. try {
  167. return JSON.parse(localStorage.getItem(localStoragePrefix + key));
  168. } catch {
  169. return null;
  170. }
  171. }
  172.  
  173. function getSignatureTimestamp() {
  174. return (
  175. getYtcfgValue('STS')
  176. || (() => {
  177. var _document$querySelect;
  178. // STS is missing on embedded player. Retrieve from player base script as fallback...
  179. const playerBaseJsPath = (_document$querySelect = document.querySelector('script[src*="/base.js"]')) === null || _document$querySelect === void 0
  180. ? void 0
  181. : _document$querySelect.src;
  182.  
  183. if (!playerBaseJsPath) return;
  184.  
  185. const xmlhttp = new XMLHttpRequest();
  186. xmlhttp.open('GET', playerBaseJsPath, false);
  187. xmlhttp.send(null);
  188.  
  189. return parseInt(xmlhttp.responseText.match(/signatureTimestamp:([0-9]*)/)[1]);
  190. })()
  191. );
  192. }
  193.  
  194. function getCurrentVideoStartTime(currentVideoId) {
  195. // Check if the URL corresponds to the requested video
  196. // This is not the case when the player gets preloaded for the next video in a playlist.
  197. if (window.location.href.includes(currentVideoId)) {
  198. var _ref;
  199. // "t"-param on youtu.be urls
  200. // "start"-param on embed player
  201. // "time_continue" when clicking "watch on youtube" on embedded player
  202. const urlParams = new URLSearchParams(window.location.search);
  203. const startTimeString = (_ref = urlParams.get('t') || urlParams.get('start') || urlParams.get('time_continue')) === null || _ref === void 0
  204. ? void 0
  205. : _ref.replace('s', '');
  206.  
  207. if (startTimeString && !isNaN(startTimeString)) {
  208. return parseInt(startTimeString);
  209. }
  210. }
  211.  
  212. return 0;
  213. }
  214.  
  215. function getUnlockStrategies(videoId, reason) {
  216. const clientName = getYtcfgValue('INNERTUBE_CLIENT_NAME') || 'WEB';
  217. const clientVersion = getYtcfgValue('INNERTUBE_CLIENT_VERSION') || '2.20220203.04.00';
  218. const signatureTimestamp = getSignatureTimestamp();
  219. const startTimeSecs = getCurrentVideoStartTime(videoId);
  220. const hl = getYtcfgValue('HL');
  221.  
  222. return [
  223. {
  224. name: 'ios',
  225. requiresAuth: true,
  226. payload: {
  227. context: {
  228. client: {
  229. clientName: 'IOS',
  230. clientVersion: '19.09.3',
  231. deviceModel: 'iPhone14,3',
  232. // check https://github.com/yt-dlp/yt-dlp/blob/master/yt_dlp/extractor/youtube.py#L176 for client name/ver updates
  233. // userAgent: 'com.google.ios.youtube/19.09.3 (iPhone14,3; U; CPU iOS 15_6 like Mac OS X)',
  234. hl,
  235. },
  236. },
  237. playbackContext: {
  238. contentPlaybackContext: {
  239. signatureTimestamp,
  240. },
  241. },
  242. videoId,
  243. startTimeSecs,
  244. racyCheckOk: true,
  245. contentCheckOk: true,
  246. },
  247. endpoint: innertube,
  248. },
  249. ]
  250. }
  251.  
  252. let cachedPlayerResponse = {};
  253.  
  254. function createDeepCopy(obj) {
  255. return nativeJSONParse(JSON.stringify(obj));
  256. }
  257.  
  258. function isUserLoggedIn() {
  259. // LOGGED_IN doesn't exist on embedded page, use DELEGATED_SESSION_ID or SESSION_INDEX as fallback
  260. if (typeof getYtcfgValue('LOGGED_IN') === 'boolean') return getYtcfgValue('LOGGED_IN');
  261. if (typeof getYtcfgValue('DELEGATED_SESSION_ID') === 'string') return true;
  262. if (parseInt(getYtcfgValue('SESSION_INDEX')) >= 0) return true;
  263.  
  264. return false;
  265. }
  266.  
  267. function getUnlockedPlayerResponse(videoId, reason, copy) {
  268. // Check if response is cached
  269. // if (cachedPlayerResponse.videoId === videoId) return createDeepCopy(cachedPlayerResponse);
  270. if (cachedPlayerResponse.videoId === videoId && !copy) {
  271. try {
  272. // check if hls manifest expired on the cached response
  273. // for the edge case of pausing a video at night and continuing it next morning
  274. const expireDate = cachedPlayerResponse.streamingData.hlsManifestUrl.match(/(?<=expire\/)\d+/)[0]
  275. const initialSecondsLeft = cachedPlayerResponse.streamingData.expiresInSeconds // 21540, almost 6h. This is a minute before reaching expire date
  276. const offset = 100
  277. const secondsNow = Math.floor(Date.now() / 1000)
  278. const age = expireDate - secondsNow - offset
  279.  
  280. if (initialSecondsLeft - (initialSecondsLeft - age) < 0) {
  281. console.log('cached player response expired, refetching ...')
  282. } else {
  283. console.log(
  284. 'using cached response',
  285. // cachedPlayerResponse,
  286. )
  287. return createDeepCopy(cachedPlayerResponse);
  288. }
  289. } catch(err) {
  290. console.log('failed to check cached response age, page reload might be necessary', err)
  291. return createDeepCopy(cachedPlayerResponse);
  292. }
  293. }
  294.  
  295. const unlockStrategies = getUnlockStrategies(videoId, reason);
  296.  
  297. let unlockedPlayerResponse = {};
  298.  
  299. // Try every strategy until one of them works
  300. unlockStrategies.every((strategy, index) => {
  301. var _unlockedPlayerRespon6;
  302. // Skip strategy if authentication is required and the user is not logged in
  303. // if (strategy.skip || strategy.requiresAuth && !isUserLoggedIn()) return true;
  304.  
  305. console.log(`Trying Player Unlock Method #${index + 1} (${strategy.name})`);
  306.  
  307. try {
  308. unlockedPlayerResponse = strategy.endpoint.getPlayer(strategy.payload, strategy.requiresAuth || strategy.optionalAuth);
  309. } catch (err) {
  310. console.log(err, `Player Unlock Method ${index + 1} failed with exception`);
  311. }
  312.  
  313. const isStatusValid = Config.VALID_PLAYABILITY_STATUSES.includes(
  314. (_unlockedPlayerRespon6 = unlockedPlayerResponse) === null || _unlockedPlayerRespon6 === void 0
  315. || (_unlockedPlayerRespon6 = _unlockedPlayerRespon6.playabilityStatus) === null || _unlockedPlayerRespon6 === void 0
  316. ? void 0
  317. : _unlockedPlayerRespon6.status,
  318. );
  319.  
  320. if (isStatusValid) {
  321. var _unlockedPlayerRespon7;
  322. /**
  323. * Workaround: https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/issues/191
  324. *
  325. * YouTube checks if the `trackingParams` in the response matches the decoded `trackingParam` in `responseContext.mainAppWebResponseContext`.
  326. * However, sometimes the response does not include the `trackingParam` in the `responseContext`, causing the check to fail.
  327. *
  328. * This workaround addresses the issue by hardcoding the `trackingParams` in the response context.
  329. */
  330. if (
  331. !unlockedPlayerResponse.trackingParams
  332. || !((_unlockedPlayerRespon7 = unlockedPlayerResponse.responseContext) !== null && _unlockedPlayerRespon7 !== void 0
  333. && (_unlockedPlayerRespon7 = _unlockedPlayerRespon7.mainAppWebResponseContext) !== null && _unlockedPlayerRespon7 !== void 0
  334. && _unlockedPlayerRespon7.trackingParam)
  335. ) {
  336. unlockedPlayerResponse.trackingParams = 'CAAQu2kiEwjor8uHyOL_AhWOvd4KHavXCKw=';
  337. unlockedPlayerResponse.responseContext = {
  338. mainAppWebResponseContext: {
  339. trackingParam: 'kx_fmPxhoPZRzgL8kzOwANUdQh8ZwHTREkw2UqmBAwpBYrzRgkuMsNLBwOcCE59TDtslLKPQ-SS',
  340. },
  341. };
  342. }
  343.  
  344. /**
  345. * Workaround: Account proxy response currently does not include `playerConfig`
  346. *
  347. * Stays here until we rewrite the account proxy to only include the necessary and bare minimum response
  348. */
  349. if (strategy.payload.startTimeSecs && strategy.name === 'Account Proxy') {
  350. unlockedPlayerResponse.playerConfig = {
  351. playbackStartConfig: {
  352. startSeconds: strategy.payload.startTimeSecs,
  353. },
  354. };
  355. }
  356. }
  357.  
  358. return !isStatusValid;
  359. });
  360.  
  361. // Cache response to prevent a flood of requests in case youtube processes a blocked response mutiple times.
  362. if (!copy) {
  363. cachedPlayerResponse = { videoId, ...createDeepCopy(unlockedPlayerResponse) };
  364. }
  365.  
  366. return unlockedPlayerResponse;
  367. }
  368.  
  369. let lastPlayerUnlockVideoId = null;
  370. let lastPlayerUnlockReason = null;
  371.  
  372. function waitForElement(elementSelector, timeout) {
  373. const deferred = new Deferred();
  374.  
  375. const checkDomInterval = setInterval(() => {
  376. const elem = document.querySelector(elementSelector);
  377. if (elem) {
  378. clearInterval(checkDomInterval);
  379. deferred.resolve(elem);
  380. }
  381. }, 100);
  382.  
  383. if (timeout) {
  384. setTimeout(() => {
  385. clearInterval(checkDomInterval);
  386. deferred.reject();
  387. }, timeout);
  388. }
  389.  
  390. return deferred;
  391. }
  392. // const nativeJSONParse = window.JSON.parse;
  393. // const nativeXMLHttpRequestOpen = window.XMLHttpRequest.prototype.open;
  394. const nativeJSONParse = unsafeWindow.JSON.parse;
  395. const nativeXMLHttpRequestOpen = unsafeWindow.XMLHttpRequest.prototype.open;
  396.  
  397. const isDesktop = window.location.host !== 'm.youtube.com';
  398. const isMusic = window.location.host === 'music.youtube.com';
  399. const isEmbed = window.location.pathname.indexOf('/embed/') === 0;
  400.  
  401. function createElement(tagName, options) {
  402. const node = document.createElement(tagName);
  403. options && Object.assign(node, options);
  404. return node;
  405. }
  406.  
  407. class Deferred {
  408. constructor() {
  409. return Object.assign(
  410. new Promise((resolve, reject) => {
  411. this.resolve = resolve;
  412. this.reject = reject;
  413. }),
  414. this,
  415. );
  416. }
  417. }
  418.  
  419. function pageLoaded() {
  420. if (document.readyState === 'complete') return Promise.resolve();
  421.  
  422. const deferred = new Deferred();
  423.  
  424. unsafeWindow.addEventListener('load', deferred.resolve, { once: true });
  425.  
  426. return deferred;
  427. }
  428.  
  429. var tDesktop = '<tp-yt-paper-toast></tp-yt-paper-toast>\n';
  430.  
  431. var tMobile =
  432. '<c3-toast>\n <ytm-notification-action-renderer>\n <div class="notification-action-response-text"></div>\n </ytm-notification-action-renderer>\n</c3-toast>\n';
  433.  
  434. const template = isDesktop ? tDesktop : tMobile;
  435.  
  436. const nToastContainer = createElement('div', { id: 'toast-container', innerHTML: template });
  437. const nToast = nToastContainer.querySelector(':scope > *');
  438.  
  439. async function show(message, duration = 5) {
  440. // if (!Config.ENABLE_UNLOCK_NOTIFICATION) return;
  441. if (isEmbed) return;
  442.  
  443. await pageLoaded();
  444.  
  445. // Do not show notification when tab is in background
  446. if (document.visibilityState === 'hidden') return;
  447.  
  448. // Append toast container to DOM, if not already done
  449. if (!nToastContainer.isConnected) document.documentElement.append(nToastContainer);
  450.  
  451. nToast.duration = duration * 1000;
  452. nToast.show(message);
  453. }
  454.  
  455. var Toast = { show };
  456.  
  457. const messagesMap = {
  458. success: 'hls manifest available',
  459. fail: 'Failed to fetch hls manifest',
  460. _616: '616 available',
  461. };
  462.  
  463. function isPlayerObject(parsedData) {
  464. return (parsedData === null || parsedData === void 0 ? void 0 : parsedData.videoDetails)
  465. && (parsedData === null || parsedData === void 0 ? void 0 : parsedData.playabilityStatus);
  466. }
  467.  
  468. function isPremium1080pAvailable(parsedData) {
  469. return parsedData?.paygatedQualitiesMetadata?.qualityDetails?.reduce((found, current) => {
  470. if (current.key === '1080p Premium') {
  471. return current
  472. }
  473. return found
  474. }, undefined)
  475. }
  476.  
  477. function getYtcfgValue(name) {
  478. var _window$ytcfg;
  479. return (_window$ytcfg = unsafeWindow.ytcfg) === null || _window$ytcfg === void 0 ? void 0 : _window$ytcfg.get(name);
  480. }
  481.  
  482. function unlockResponse$1(playerResponse) {
  483. var _playerResponse$video, _playerResponse$playa, _playerResponse$previ, _unlockedPlayerRespon, _unlockedPlayerRespon3;
  484.  
  485. const videoId = ((_playerResponse$video = playerResponse.videoDetails) === null || _playerResponse$video === void 0 ? void 0 : _playerResponse$video.videoId)
  486. || getYtcfgValue('PLAYER_VARS').video_id;
  487. const reason = ((_playerResponse$playa = playerResponse.playabilityStatus) === null || _playerResponse$playa === void 0 ? void 0 : _playerResponse$playa.status)
  488. || ((_playerResponse$previ = playerResponse.previewPlayabilityStatus) === null || _playerResponse$previ === void 0 ? void 0 : _playerResponse$previ.status);
  489.  
  490. // if (!Config.SKIP_CONTENT_WARNINGS && reason.includes('CHECK_REQUIRED')) {
  491. // console.log(`SKIP_CONTENT_WARNINGS disabled and ${reason} status detected.`);
  492. // return;
  493. // }
  494.  
  495. lastPlayerUnlockVideoId = videoId;
  496. lastPlayerUnlockReason = reason;
  497.  
  498.  
  499. const unlockedPlayerResponse = getUnlockedPlayerResponse(videoId, reason);
  500. // console.log('ios response', unlockedPlayerResponse)
  501.  
  502. // // account proxy error?
  503. // if (unlockedPlayerResponse.errorMessage) {
  504. // Toast.show(`${messagesMap.fail} (ProxyError)`, 10);
  505. // throw new Error(`Player Unlock Failed, Proxy Error Message: ${unlockedPlayerResponse.errorMessage}`);
  506. // }
  507.  
  508. // check if the unlocked response isn't playable
  509. if (
  510. !Config.VALID_PLAYABILITY_STATUSES.includes(
  511. (_unlockedPlayerRespon = unlockedPlayerResponse.playabilityStatus) === null || _unlockedPlayerRespon === void 0 ? void 0 : _unlockedPlayerRespon.status,
  512. )
  513. ) {
  514. var _unlockedPlayerRespon2;
  515. Toast.show(`${messagesMap.fail} (PlayabilityError)`, 10);
  516. throw new Error(
  517. `Player Unlock Failed, playabilityStatus: ${
  518. (_unlockedPlayerRespon2 = unlockedPlayerResponse.playabilityStatus) === null || _unlockedPlayerRespon2 === void 0 ? void 0 : _unlockedPlayerRespon2.status
  519. }`,
  520. );
  521. }
  522.  
  523. if (!unlockedPlayerResponse.streamingData.hlsManifestUrl) {
  524. Toast.show(`${messagesMap.fail} (undefined)`, 10)
  525. throw new Error('response is playable but doesn\'t contain hls manifest (???)', unlockedPlayerResponse)
  526. }
  527.  
  528.  
  529.  
  530. // Overwrite the embedded (preview) playabilityStatus with the unlocked one
  531. if (playerResponse.previewPlayabilityStatus) {
  532. playerResponse.previewPlayabilityStatus = unlockedPlayerResponse.playabilityStatus;
  533. }
  534.  
  535. // Transfer all unlocked properties to the original player response
  536. // Object.assign(playerResponse, unlockedPlayerResponse);
  537.  
  538. playerResponse.streamingData.__hlsManifestUrl = unlockedPlayerResponse.streamingData.hlsManifestUrl
  539. // is there a player library that can play dash, hls and mix and match by selecting video and audio streams? like playing 616+251
  540. // playerResponse.streamingData.__adaptiveFormats = unlockedPlayerResponse.streamingData.adaptiveFormats
  541.  
  542.  
  543. // playerResponse.playabilityStatus.paygatedQualitiesMetadata.qualityDetails[0].value = {} // this closes the popup after click and selects normal 1080p
  544. // playerResponse.playabilityStatus.paygatedQualitiesMetadata.qualityDetails[0].value.paygatedIndicatorText = 'HLS Manifest'
  545. // playerResponse.playabilityStatus.paygatedQualitiesMetadata.qualityDetails[0].value.endpoint = {} // remove popup on click, do nothing
  546. // playerResponse.playabilityStatus.paygatedQualitiesMetadata.restrictedAdaptiveFormats = [] // this removed the option alltogether
  547.  
  548.  
  549. // playerResponse.unlocked = true;
  550.  
  551. console.log('set hls manifest')
  552. if (notifyOnSuccess) {
  553. Toast.show(messagesMap.success, 2);
  554. }
  555. }
  556.  
  557. /**
  558. * Handles XMLHttpRequests and
  559. * - Rewrite Googlevideo URLs to Proxy URLs (if necessary)
  560. * - Store auth headers for the authentication of further unlock requests.
  561. * - Add "content check ok" flags to request bodys
  562. */
  563. function handleXhrOpen(method, url, xhr) {
  564. const url_obj = new URL(url);
  565. // let proxyUrl = unlockGoogleVideo(url_obj);
  566. // if (proxyUrl) {
  567. // // Exclude credentials from XMLHttpRequest
  568. // Object.defineProperty(xhr, 'withCredentials', {
  569. // set: () => {},
  570. // get: () => false,
  571. // });
  572. // return proxyUrl.toString();
  573. // }
  574.  
  575. if (url_obj.pathname.indexOf('/youtubei/') === 0) {
  576. // Store auth headers in storage for further usage.
  577. attach$4(xhr, 'setRequestHeader', ([headerName, headerValue]) => {
  578. if (Config.GOOGLE_AUTH_HEADER_NAMES.includes(headerName)) {
  579. set(headerName, headerValue);
  580. }
  581. });
  582. }
  583.  
  584. // if (Config.SKIP_CONTENT_WARNINGS && method === 'POST' && ['/youtubei/v1/player', '/youtubei/v1/next'].includes(url_obj.pathname)) {
  585. // // Add content check flags to player and next request (this will skip content warnings)
  586. // attach$4(xhr, 'send', (args) => {
  587. // if (typeof args[0] === 'string') {
  588. // args[0] = setContentCheckOk(args[0]);
  589. // }
  590. // });
  591. // }
  592. }
  593.  
  594. /**
  595. * Handles Fetch requests and
  596. * - Rewrite Googlevideo URLs to Proxy URLs (if necessary)
  597. * - Store auth headers for the authentication of further unlock requests.
  598. * - Add "content check ok" flags to request bodys
  599. */
  600. function handleFetchRequest(url, requestOptions) {
  601. const url_obj = new URL(url);
  602. // const newGoogleVideoUrl = unlockGoogleVideo(url_obj);
  603. // if (newGoogleVideoUrl) {
  604. // // Exclude credentials from Fetch Request
  605. // if (requestOptions.credentials) {
  606. // requestOptions.credentials = 'omit';
  607. // }
  608. // return newGoogleVideoUrl.toString();
  609. // }
  610.  
  611. if (url_obj.pathname.indexOf('/youtubei/') === 0 && isObject(requestOptions.headers)) {
  612. // Store auth headers in authStorage for further usage.
  613. for (let headerName in requestOptions.headers) {
  614. if (Config.GOOGLE_AUTH_HEADER_NAMES.includes(headerName)) {
  615. set(headerName, requestOptions.headers[headerName]);
  616. }
  617. }
  618. }
  619.  
  620. // if (Config.SKIP_CONTENT_WARNINGS && ['/youtubei/v1/player', '/youtubei/v1/next'].includes(url_obj.pathname)) {
  621. // // Add content check flags to player and next request (this will skip content warnings)
  622. // requestOptions.body = setContentCheckOk(requestOptions.body);
  623. // }
  624. }
  625.  
  626. function processYtData(ytData) {
  627. try {
  628. // if (isPlayerObject(ytData) && isPremium1080pAvailable(ytData.playabilityStatus)) {
  629. // if (!ytData.streamingData.__hlsManifestUrl) {
  630. // unlockResponse$1(ytData)
  631. // console.log('baa', ytData)
  632. // }
  633. // }
  634.  
  635.  
  636. // if (isPlayerObject(ytData)) {
  637. // if (isPremium1080pAvailable(ytData)) {
  638. // console.log('si prem')
  639. // // console.log(value, 'set', value.videoDetails.videoId)
  640. // if (!ytData.streamingData.__hlsManifestUrl) {
  641. // const id = ytData.videoDetails.videoId
  642. // // getIosResponse(id, ytData)
  643. // unlockResponse$1(ytData)
  644. // }
  645. // } else {
  646. // console.log('ni prem')
  647. // }
  648. // }
  649. } catch (err) {
  650. // console.log(err, 'Premium 1080p unlock failed')
  651. }
  652.  
  653. return ytData;
  654. }
  655.  
  656. try {
  657. attach$3(processYtData);
  658. attach$2(processYtData);
  659. attach(handleXhrOpen);
  660. attach$1(handleFetchRequest);
  661.  
  662. } catch (err) {
  663. console.log(err, 'Error while attaching data interceptors');
  664. }
  665.  
  666. function attach$4(obj, prop, onCall) {
  667. if (!obj || typeof obj[prop] !== 'function') {
  668. return;
  669. }
  670.  
  671. let original = obj[prop];
  672.  
  673. obj[prop] = function() {
  674. try {
  675. onCall(arguments);
  676. } catch {}
  677. original.apply(this, arguments);
  678. };
  679. }
  680.  
  681. let ageRestricted = false
  682. let live = false
  683. function attach$3(onInitialData) {
  684. interceptObjectProperty('playerResponse', (obj, playerResponse) => {
  685. // console.log(`playerResponse property set, contains sidebar: ${!!obj.response}`);
  686.  
  687. // The same object also contains the sidebar data and video description
  688. if (isObject(obj.response)) onInitialData(obj.response);
  689.  
  690. // If the script is executed too late and the bootstrap data has already been processed,
  691. // a reload of the player can be forced by creating a deep copy of the object.
  692. // This is especially relevant if the userscript manager does not handle the `@run-at document-start` correctly.
  693. // playerResponse.unlocked = false;
  694.  
  695. onInitialData(playerResponse);
  696.  
  697.  
  698.  
  699.  
  700. const id = playerResponse?.videoDetails?.videoId
  701. // don't run on unavailable videos
  702. // don't run when hovering over videos on the youtube home page
  703. // don't run on unavailable videos (no streaming data)
  704. if (
  705. id &&
  706. location.href.includes(id) &&
  707. playerResponse.streamingData &&
  708. playerResponse.videoDetails
  709. ) {
  710. if (id !== sharedPlayerElements.id) {
  711. ageRestricted = !!playerResponse.unlocked || !!playerResponse.YHEageRestricted // for cached responses
  712. live = !!playerResponse.videoDetails.isLive
  713. console.log(
  714. '-----------------------------------------------------\nnew vid',
  715. id,
  716. '\nis live:',
  717. live,
  718. '\nis SYARB unlocked:',
  719. ageRestricted,
  720. // playerResponse,
  721. )
  722. resetPlayer()
  723. sharedPlayerElements.hlsUrl = false
  724. // mark response as ageRestricted so we know if we meet it agan from cache without playerResponse.unlocked
  725. ageRestricted && (playerResponse.YHEageRestricted = true)
  726. }
  727.  
  728. // don't run when https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass unlocked the video
  729. // don't run on live content
  730. if (!ageRestricted && !live && (isPremium1080pAvailable(playerResponse.playabilityStatus) || !onlyOnPremiumAvailable)) {
  731. if (!playerResponse.streamingData.__hlsManifestUrl) {
  732. unlockResponse$1(playerResponse)
  733. // console.log('unlock fn, obj', unlockResponse$1, playerResponse)
  734. // sharedPlayerElements.hlsUrl = playerResponse.streamingData.__hlsManifestUrl
  735. }
  736. sharedPlayerElements.hlsUrl = playerResponse.streamingData.__hlsManifestUrl
  737. setupPlayer()
  738. }
  739.  
  740. sharedPlayerElements.id = id
  741. }
  742. currentVideoId = id
  743.  
  744.  
  745.  
  746.  
  747. // return playerResponse.unlocked ? createDeepCopy(playerResponse) : playerResponse;
  748. return playerResponse
  749. });
  750.  
  751. // The global `ytInitialData` variable can be modified on the fly.
  752. // It contains search results, sidebar data and meta information
  753. // Not really important but fixes https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/issues/127
  754. unsafeWindow.addEventListener('DOMContentLoaded', () => {
  755. if (isObject(unsafeWindow.ytInitialData)) {
  756. onInitialData(unsafeWindow.ytInitialData);
  757. }
  758. });
  759. }
  760.  
  761. function attach$2(onJsonDataReceived) {
  762. unsafeWindow.JSON.parse = function() {
  763. const data = nativeJSONParse.apply(this, arguments);
  764. return isObject(data) ? onJsonDataReceived(data) : data;
  765. };
  766. }
  767.  
  768. function attach$1(onRequestCreate) {
  769. if (typeof unsafeWindow.Request !== 'function') {
  770. return;
  771. }
  772.  
  773. unsafeWindow.Request = new Proxy(unsafeWindow.Request, {
  774. construct(target, args) {
  775. let [url, options] = args;
  776. try {
  777. if (typeof url === 'string') {
  778. if (url.indexOf('/') === 0) {
  779. url = window.location.origin + url;
  780. }
  781.  
  782. if (url.indexOf('https://') !== -1) {
  783. const modifiedUrl = onRequestCreate(url, options);
  784.  
  785. if (modifiedUrl) {
  786. args[0] = modifiedUrl;
  787. }
  788. }
  789. }
  790. } catch (err) {
  791. console.log(err, `Failed to intercept Request()`);
  792. }
  793.  
  794. return Reflect.construct(target, args);
  795. },
  796. });
  797. }
  798.  
  799. function attach(onXhrOpenCalled) {
  800. unsafeWindow.XMLHttpRequest.prototype.open = function(...args) {
  801. let [method, url] = args;
  802. try {
  803. if (typeof url === 'string') {
  804. if (url.indexOf('/') === 0) {
  805. url = window.location.origin + url;
  806. }
  807.  
  808. if (url.indexOf('https://') !== -1) {
  809. const modifiedUrl = onXhrOpenCalled(method, url, this);
  810.  
  811. if (modifiedUrl) {
  812. args[1] = modifiedUrl;
  813. }
  814. }
  815. }
  816. } catch (err) {
  817. console.log(err, `Failed to intercept XMLHttpRequest.open()`);
  818. }
  819.  
  820. nativeXMLHttpRequestOpen.apply(this, args);
  821. };
  822. }
  823.  
  824.  
  825.  
  826. function isObject(obj) {
  827. return obj !== null && typeof obj === 'object';
  828. }
  829.  
  830. function interceptObjectProperty(prop, onSet) {
  831. var _Object$getOwnPropert;
  832. // Allow other userscripts to decorate this descriptor, if they do something similar
  833. // const dataKey = '__SYARB_' + prop;
  834. const dataKey = '__1080pp_' + prop;
  835. const { get: getter, set: setter } = (_Object$getOwnPropert = Object.getOwnPropertyDescriptor(Object.prototype, prop)) !== null && _Object$getOwnPropert !== void 0
  836. ? _Object$getOwnPropert
  837. : {
  838. set(value) {
  839. this[dataKey] = value;
  840. },
  841. get() {
  842. return this[dataKey];
  843. },
  844. };
  845.  
  846. // Intercept the given property on any object
  847. // The assigned attribute value and the context (enclosing object) are passed to the onSet function.
  848. Object.defineProperty(Object.prototype, prop, {
  849. set(value) {
  850. setter.call(this, isObject(value) ? onSet(this, value) : value);
  851. },
  852. get() {
  853. return getter.call(this);
  854. },
  855. configurable: true,
  856. });
  857. }
  858.  
  859. // const hls = new Hls() // api guide at https://github.com/video-dev/hls.js/blob/master/docs/API.md
  860.  
  861. // method 1
  862. /* class fLoader extends Hls.DefaultConfig.loader {
  863. constructor(config) {
  864. super(config);
  865. const load = this.load.bind(this);
  866. this.load = function (context, config, callbacks) {
  867. // console.log(...arguments)
  868. const onError = callbacks.onError
  869. callbacks.onError = function (error, context, xhr) {
  870. // hls.js doesn' retry on code 0 cors error, change it here for shouldRetry to be called next
  871. // https://github.com/video-dev/hls.js/blob/773fe886ed45cc83a015045c314763953b9a49d9/src/utils/error-helper.ts#L77
  872.  
  873. console.log('err', ...arguments, 'errrrr', this.requestTimeout)
  874. if (error.code === 0 && new URL(context.url).hostname.endsWith('.googlevideo.com')) {
  875. GM_xmlhttpRequest({
  876. url: context.url,
  877. onload: function (r) {
  878. if (r.status === 200 && r.finalUrl !== context.url) {
  879. error.code = 302
  880. error.recoverable = true // this gets passed to shouldRetry
  881. // context.frag._url is the url used if shouldRetry returns true
  882. context.frag._url = r.finalUrl
  883. onError(error, context, xhr)
  884. }
  885. },
  886. onerror: function (r) {
  887. console.log(
  888. 'Failed to recover cors error',
  889. r,
  890. )
  891. onError(error, context, xhr)
  892. }
  893. })
  894. } else {
  895. onError(error, context, xhr)
  896. }
  897. }
  898. load(context, config, callbacks);
  899. }
  900. }
  901. } */
  902.  
  903. // method 3
  904. // fLoader only runs on fragments
  905. // add .isFragment to xhr here to use it in xhrSetup
  906. class fLoader2 extends Hls.DefaultConfig.loader {
  907. constructor(config) {
  908. super(config);
  909. this.loadInternal = function() {
  910. var t = this,
  911. e = this.config,
  912. r = this.context;
  913. if (e && r) {
  914. var i = this.loader = new self.XMLHttpRequest,
  915. n = this.stats;
  916. i.isFragment = true // just adding this to the original loadInternal function, we use it in xhrSetup
  917. n.loading.first = 0, n.loaded = 0, n.aborted = !1;
  918. var a = this.xhrSetup;
  919. a ? Promise.resolve().then((function() {
  920. if (!t.stats.aborted) return a(i, r.url)
  921. })).catch((function(t) {
  922. return i.open("GET", r.url, !0), a(i, r.url)
  923. })).then((function() {
  924. t.stats.aborted || t.openAndSendXhr(i, r, e)
  925. })).catch((function(e) {
  926. t.callbacks.onError({
  927. code: i.status,
  928. text: e.message
  929. }, r, i, n)
  930. })) : this.openAndSendXhr(i, r, e)
  931. }
  932. }.bind(this)
  933. }
  934. }
  935. // const desc = Object.getOwnPropertyDescriptor(Hls.DefaultConfig.abrController.prototype, "nextAutoLevel")
  936. //
  937. // Object.defineProperty(Hls.DefaultConfig.abrController.prototype, "nextAutoLevel", {
  938. // get: desc.get,
  939. // set: new Proxy(desc.set, {
  940. // apply(target, thisArg, args) {
  941. // console.log('set nextautolvl', ...arguments, 'bb', )
  942. // return Reflect.apply(...arguments)
  943. // }
  944. // })
  945. // })
  946.  
  947. const hls = new Hls({
  948. // sometimes segment urls redirect to a different url (usually 234 audio)
  949. // the redirect loses the Origin request header and we get blocked by cors
  950. // https://stackoverflow.com/a/22625354 - related info
  951.  
  952. // workaround method 1 requests redirecting fragments twice, once via gm_xhr to get the final url
  953. // and hls.js requests the final url again.
  954. // inefficient but nothing compared to the amount of abuse the yt fragment urls can take.
  955. // :::: it seems it breaks the built in hls.js autobitrate controller and sometimes gets stuck on low quality
  956. // :::: makes hls.bandwidthEstimate grow forever?
  957. //
  958. // check git here for older discarded workaround ideas if the current one fails later on
  959. debug: false,
  960. // startLevel: 15, // this gets the player stuck if your internet isn't up to par with 616, or 1440p if 616 is not available
  961. // startFragPrefetch: true,
  962. abrEwmaDefaultEstimate: 5000000, // bump to 5MBps from 0.5 MBps default, reasonable if you have good enough internet to consider this userscript i'd say
  963. // also doesn't take away the ability to auto adjust to lower res if needed
  964.  
  965. // methon 1
  966. /* fragLoadPolicy: {
  967. default: {
  968. maxTimeToFirstByteMs: 9000,
  969. maxLoadTimeMs: 100000,
  970. timeoutRetry: {
  971. maxNumRetry: 2,
  972. retryDelayMs: 0,
  973. maxRetryDelayMs: 0,
  974. },
  975. errorRetry: {
  976. maxNumRetry: 5,
  977. retryDelayMs: 3000,
  978. maxRetryDelayMs: 15000,
  979. backoff: 'linear',
  980. // can't find a way to define shouldRetry alone without this entire block
  981. shouldRetry: function(retryConfig, retryCount, isTimeout, loaderResponse, originalShouldRetryResponse) {
  982. if (loaderResponse.recoverable) {
  983. console.log(
  984. 'Retrying recoverable cors error. Attempt nr:',
  985. retryCount,
  986. )
  987. // retryConfig.retryDelayMs = 150
  988. retryConfig.retryDelayMs = 0 // hmm, this actually changes the entire config
  989. retryConfig.maxRetryDelayMs = 0
  990. return true
  991. }
  992. retryConfig.retryDelayMs = 3000
  993. retryConfig.maxRetryDelayMs = 15000
  994. return originalShouldRetryResponse
  995. }
  996. },
  997. },
  998. },
  999. fLoader: fLoader, */
  1000.  
  1001.  
  1002. fLoader: fLoader2,
  1003. xhrSetup(xhr, url) {
  1004.  
  1005. // method 2
  1006. // this block alone works perfectly but requests everything twice so it is slower
  1007. /* return new Promise(function(resolve, reject) {
  1008. // console.log('req')
  1009. GM_xmlhttpRequest({
  1010. url: url,
  1011. onload: function(r) {
  1012. // console.log('loaded')
  1013. if (r.status === 200) {
  1014. xhr.open('GET', r.finalUrl)
  1015. resolve()
  1016. }
  1017. },
  1018. onerror: function(r) {
  1019. console.log(
  1020. 'Failed to recover cors error',
  1021. r,
  1022. )
  1023. reject()
  1024. }
  1025. })
  1026. }) */
  1027.  
  1028. // method 3
  1029. // source code reference https://github.com/video-dev/hls.js/blob/773fe886ed45cc83a015045c314763953b9a49d9/src/utils/xhr-loader.ts#L153
  1030. // this only requests fragments once with gm_xhr
  1031. // seems to also work perfectly so far
  1032. if (xhr.isFragment) {
  1033. // const ogsend = xhr.send.bind(xhr)
  1034. xhr.send = function(...args) {
  1035. // console.log('sent')
  1036. xhr._onreadystatechange = xhr.onreadystatechange
  1037. xhr._onprogress = xhr.onprogress
  1038. xhr.onprogress = null
  1039. xhr.onreadystatechange = null
  1040. Object.defineProperty(xhr, "readyState", {writable: true})
  1041. Object.defineProperty(xhr, "status", {writable: true})
  1042. Object.defineProperty(xhr, "response", {writable: true})
  1043.  
  1044. // return ogsend(...args)
  1045. }
  1046.  
  1047. return new Promise(function(resolve, reject) {
  1048. // console.log('req')
  1049. GM_xmlhttpRequest({
  1050. url: url,
  1051. responseType: 'arraybuffer',
  1052. // onprogress: function(e) {
  1053. // xhr._onprogress({
  1054. // loaded: e.loaded,
  1055. // total: e.total
  1056. // })
  1057. // },
  1058. onprogress: xhr._onprogress,
  1059. onreadystatechange: function(e) {
  1060. // console.log(
  1061. // 'rsc',
  1062. // // e,
  1063. // // xhr
  1064. // )
  1065. xhr.status = e.status
  1066. xhr.readyState = e.readyState
  1067. xhr.response = e.response
  1068. xhr._onreadystatechange()
  1069. }
  1070. })
  1071. resolve()
  1072. })
  1073. }
  1074. }
  1075. })
  1076.  
  1077. const sharedPlayerElements = {}
  1078. unsafeWindow.Hls = Hls
  1079. unsafeWindow.hls = hls
  1080. unsafeWindow.sharedPlayerElements = sharedPlayerElements
  1081. // self.hls = hls
  1082. // self.sharedPlayerElements = sharedPlayerElements
  1083. function setupPlayer() {
  1084. if (sharedPlayerElements.hlsToggle) return
  1085. const div = document.createElement('div')
  1086. div.innerHTML = `<div id="yt1080pp" class="ytp-menuitem" role="menuitemcheckbox" aria-checked="false" tabindex="0"><div style="text-align: center;" class="ytp-menuitem-icon">pp</div><div class="ytp-menuitem-label"><span>Hls manifest</span><br><div style="display: none;"><span id="yt1080pp_vitag">0</span><span id="yt1080pp_va_separator">/</span><span id="yt1080pp_aitag">0</span></div></div><div class="ytp-menuitem-content"><div class="ytp-menuitem-toggle-checkbox"></div></div></div>`
  1087. const wtf = div.firstChild
  1088. if (isEmbed) {
  1089. wtf.firstChild.innerText = ''
  1090. }
  1091.  
  1092. wtf.addEventListener('click', _ => {
  1093. if (wtf.ariaChecked === 'false') {
  1094. wtf.ariaChecked = 'true'
  1095.  
  1096. // block the normal quality button
  1097. wtf.previousSibling.style.position = 'relative'
  1098. const blocker = createElement('div', {
  1099. style: 'background-color: rgba(0 0 0 / 0.5);width: 100%;height: 100%;position: absolute;top: 0;left: 0;cursor: not-allowed;',
  1100. onclick: e => {
  1101. e.stopPropagation()
  1102. e.preventDefault()
  1103. }
  1104. })
  1105. wtf.previousSibling.append(blocker)
  1106. wtf.querySelector('br').nextSibling.style.display = ''
  1107. sharedPlayerElements.blocker = blocker
  1108.  
  1109. hookHlsjs()
  1110. } else {
  1111. wtf.ariaChecked = 'false'
  1112.  
  1113. wtf.previousSibling.style.position = ''
  1114. wtf.querySelector('br').nextSibling.style.display = 'none'
  1115. sharedPlayerElements.blocker?.remove?.()
  1116. sharedPlayerElements.blocker = false
  1117.  
  1118. unhookHlsjs()
  1119. }
  1120. })
  1121.  
  1122. function panelReady() {
  1123. const panel = document.querySelector('div:not(.ytp-contextmenu) > div.ytp-panel > .ytp-panel-menu')
  1124. const vid = document.querySelector('video.html5-main-video')
  1125. const settings = document.querySelector('.ytp-settings-button')
  1126. if (panel && panel.childElementCount === 0 && settings) {
  1127. // settings panel is empty until opened when first loading the page
  1128. settings.click()
  1129. settings.click()
  1130. }
  1131. return (panel && vid && settings && panel.firstChild) ? panel : undefined
  1132. }
  1133. function addTo(target) {
  1134. target.append(wtf)
  1135. sharedPlayerElements.hlsToggle = wtf
  1136. console.log('added toggle')
  1137.  
  1138. if (onByDefault) {
  1139. wtf.click()
  1140. console.log('autostarted hls')
  1141. }
  1142.  
  1143. if (notify616 || onBy616) {
  1144. fetch(sharedPlayerElements.hlsUrl)
  1145. .then(r => r.text())
  1146. .then(r => {
  1147. const match = r.match(/\/itag\/616\//)
  1148. if (match) {
  1149. if (notify616) {
  1150. Toast.show(messagesMap._616, 2)
  1151. console.log('616 detected')
  1152. }
  1153. if (!onByDefault && onBy616) {
  1154. wtf.click()
  1155. console.log('started hls because 616')
  1156. }
  1157. }
  1158. })
  1159. }
  1160. }
  1161.  
  1162. if (panelReady()) {
  1163. // addTo(panelReady())
  1164. setTimeout(addTo.bind(null, panelReady()))
  1165. } else {
  1166. new MutationObserver(function(m) {
  1167. label: for (const i of m) {
  1168. const panel = panelReady()
  1169. if (panel) {
  1170. this.disconnect()
  1171. addTo(panel)
  1172.  
  1173. break label
  1174. }
  1175. }
  1176. }).observe(document, {subtree: true, childList: true})
  1177. }
  1178. sharedPlayerElements.hlsToggle = true
  1179. console.log('adding toggle')
  1180. }
  1181.  
  1182. function resetPlayer() {
  1183. if (sharedPlayerElements.hlsToggle) {
  1184. if (sharedPlayerElements.hlsToggle.ariaChecked === 'true') {
  1185. sharedPlayerElements.hlsToggle.click()
  1186. }
  1187. sharedPlayerElements.hlsToggle.remove()
  1188. sharedPlayerElements.hlsToggle = false
  1189. console.log('removed toggle')
  1190. }
  1191. }
  1192.  
  1193. function hookHlsjs() {
  1194. const vid = document.querySelector('video')
  1195. const time = vid.currentTime
  1196. if (vid.src) {
  1197. sharedPlayerElements.pre_hlsjs_hook_src = vid.src
  1198. }
  1199.  
  1200. hls.loadSource(sharedPlayerElements.hlsUrl)
  1201. hls.attachMedia(vid)
  1202.  
  1203. hls.on(Hls.Events.LEVEL_SWITCHED, (event, data) => {
  1204. // console.log(event, data)
  1205. const itag = hls.levels[data.level].url[0].match(/(?<=itag\/)\d+/)?.[0] || '?'
  1206. document.querySelector('#yt1080pp_vitag').innerText = itag
  1207. })
  1208.  
  1209. hls.on(Hls.Events.AUDIO_TRACK_SWITCHED , (event, data) => {
  1210. // console.log(event, data)
  1211. const itag = data?.attrs?.["GROUP-ID"] || '?'
  1212. document.querySelector('#yt1080pp_aitag').innerText = itag
  1213. })
  1214.  
  1215. hls.on(Hls.Events.ERROR, (event, data) => {
  1216. console.log(event, data)
  1217. // we can check if the error was solved in data.errorAction.resolved
  1218. if (data.fatal) {
  1219. console.log('fatal error, disabling. A page reload might fix this')
  1220. Toast.show('fatal playback error')
  1221. if (sharedPlayerElements.hlsToggle.ariaChecked === 'true') {
  1222. sharedPlayerElements.hlsToggle.click()
  1223. }
  1224. }
  1225. // should self disable if we can't play because cors issues or anything else really
  1226. })
  1227.  
  1228. vid.currentTime = time
  1229. vid.pause()
  1230. vid.play()
  1231. }
  1232.  
  1233. function unhookHlsjs() {
  1234. const vid = hls.media
  1235. hls.detachMedia(vid) // this also removes the src attribute
  1236.  
  1237. if (sharedPlayerElements.pre_hlsjs_hook_src) {
  1238. vid.src = sharedPlayerElements.pre_hlsjs_hook_src
  1239. delete sharedPlayerElements.pre_hlsjs_hook_src
  1240. }
  1241. // vid.src = undefined // it seems youtube fixes this almost instantly
  1242. }
  1243.  
  1244.  
  1245.  
  1246.  
  1247. let currentVideoId
  1248. let menuCommandId = 'copyHls'
  1249. const opts = {
  1250. id: menuCommandId,
  1251. autoClose: false,
  1252. }
  1253. const initialCaption = 'Copy new hls manifest'
  1254. function menuCommandFn() {
  1255. console.log('copy new hls manifest clicked')
  1256. menuCommandId = GM_registerMenuCommand('Fetching...', _ => {}, opts)
  1257. const newResponse = getUnlockedPlayerResponse(currentVideoId, '', true)
  1258. const url = newResponse?.streamingData?.hlsManifestUrl
  1259. if (url) {
  1260. GM_setClipboard(url, 'text/plain')
  1261. menuCommandId = GM_registerMenuCommand('Copied!', _ => {}, opts)
  1262. setTimeout(
  1263. _ => { menuCommandId = GM_registerMenuCommand(initialCaption, menuCommandFn, opts) },
  1264. 1000
  1265. )
  1266. return
  1267. }
  1268. menuCommandId = GM_registerMenuCommand('Error!', _ => {}, opts)
  1269. console.log('failed to copy hls manifest', newResponse)
  1270. setTimeout(
  1271. _ => { menuCommandId = GM_registerMenuCommand(initialCaption, menuCommandFn, opts) },
  1272. 3000
  1273. )
  1274. }
  1275. menuCommandId = GM_registerMenuCommand(initialCaption, menuCommandFn, opts)