Twitch Max Quality

Always forces Twitch to the highest available quality (Source / 1080p / 1440p) and instantly restores it after ads, channel switches, player reloads or tab switches. No need to touch the quality menu.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Twitch Max Quality
// @name:en      Twitch Max Quality
// @name:ru      Twitch Max Quality: всегда максимальное качество (1080p/1440p)
// @namespace    https://greasyfork.org/scripts/twitch-force-max-quality
// @version      16.2
// @description  Always forces Twitch to the highest available quality (Source / 1080p / 1440p) and instantly restores it after ads, channel switches, player reloads or tab switches. No need to touch the quality menu.
// @description:en  Always forces Twitch to the highest available quality (Source / 1080p / 1440p) and instantly restores it after ads, channel switches, player reloads or tab switches. No need to touch the quality menu.
// @description:ru  Всегда ставит максимальное качество Twitch (Source / 1080p / 1440p) и мгновенно возвращает его после рекламы, смены канала, перезагрузки плеера или переключения вкладки. Без ручного выбора в меню.
// @author       vector010
// @match        *://*.twitch.tv/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=twitch.tv
// @run-at       document-start
// @noframes
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
	'use strict';

	// Поставь 1080, если нет H.265/HEVC и 1440p даёт чёрный экран.
	const MAX_HEIGHT = Infinity;

	function getFiber(el) {
		if (!el) return null;
		for (const k in el) {
			if (k.startsWith('__reactFiber$') || k.startsWith('__reactInternalInstance$')) return el[k];
		}
		return null;
	}

	function findPlayer() {
		const sels = ['video', '[data-a-target="video-player"]', '.video-player', 'div[data-a-player-state]'];
		let fiber = null;
		for (const s of sels) { fiber = getFiber(document.querySelector(s)); if (fiber) break; }
		if (!fiber) return null;
		const seen = new Set();
		const stack = [fiber];
		let guard = 0;
		while (stack.length && guard++ < 30000) {
			const node = stack.pop();
			if (!node || seen.has(node)) continue;
			seen.add(node);
			for (const p of [node.memoizedProps, node.pendingProps, node.stateNode]) {
				if (!p) continue;
				const cand = p.mediaPlayerInstance || p.player ||
					((typeof p.setQuality === 'function' || typeof p.getQualities === 'function') ? p : null);
				if (cand && typeof cand.getQualities === 'function' && typeof cand.setQuality === 'function') return cand;
			}
			if (node.child) stack.push(node.child);
			if (node.sibling) stack.push(node.sibling);
			if (node.return) stack.push(node.return);
		}
		return null;
	}

	function getQualities(p) {
		try { return p.getQualities() || []; } catch (e) {}
		try { return (p.core && p.core.getQualities()) || []; } catch (e) {}
		return [];
	}

	function currentName(p) {
		try {
			const q = p.getQuality && p.getQuality();
			return q && (q.name || q.group || (typeof q === 'string' ? q : null));
		} catch (e) { return null; }
	}

	// ВАЖНО: новый плеер Twitch (Amazon IVS) ждёт ОБЪЕКТ качества, а не строку.
	function applyQuality(p, q) {
		try { if (typeof p.setAutoQualityMode === 'function') p.setAutoQualityMode(false); } catch (e) {}
		try { p.setQuality(q, false); return true; } catch (e) {}   // IVS: объект + adaptive=false
		try { p.setQuality(q); return true; } catch (e) {}          // IVS: объект
		try { p.setQuality(q.group); return true; } catch (e) {}    // старый API: строка группы
		return false;
	}

	function pickMax() {
		const p = findPlayer();
		if (!p) return false;
		const qs = getQualities(p);
		if (!qs.length) return false;
		let real = qs.filter(q => q && String(q.name || '').toLowerCase() !== 'auto' && q.group !== 'auto');
		if (isFinite(MAX_HEIGHT)) real = real.filter(q => (q.height || 0) <= MAX_HEIGHT);
		if (!real.length) return false;
		const best = real.slice().sort((a, b) =>
			((b.height || 0) - (a.height || 0)) || ((b.bitrate || 0) - (a.bitrate || 0)))[0];
		if (!best) return false;
		const cur = currentName(p);
		if (cur && best.name && cur === best.name) return true; // уже на максимуме
		return applyQuality(p, best);
	}

	// мягкие повторы: останавливаемся при успехе, не спамим
	let timer = null, until = 0, okCount = 0;
	function burst(durationMs = 10000, stepMs = 400) {
		until = Math.max(until, Date.now() + durationMs);
		okCount = 0;
		if (timer) return;
		timer = setInterval(() => {
			let done = false;
			try { done = pickMax(); } catch (e) {}
			if (done) { okCount++; if (okCount >= 2) { clearInterval(timer); timer = null; return; } }
			if (Date.now() > until) { clearInterval(timer); timer = null; }
		}, stepMs);
	}

	function onNav() { burst(10000, 400); }
	const _push = history.pushState;
	history.pushState = function () { const r = _push.apply(this, arguments); dispatchEvent(new Event('tmq:nav')); return r; };
	const _rep = history.replaceState;
	history.replaceState = function () { const r = _rep.apply(this, arguments); dispatchEvent(new Event('tmq:nav')); return r; };
	addEventListener('popstate', onNav);
	addEventListener('tmq:nav', onNav);

	let lastVideo = null, scheduled = false;
	new MutationObserver(() => {
		if (scheduled) return;
		scheduled = true;
		requestAnimationFrame(() => {
			scheduled = false;
			const v = document.querySelector('video');
			if (v && v !== lastVideo) { lastVideo = v; burst(10000, 400); }
		});
	}).observe(document.documentElement, { childList: true, subtree: true });

	addEventListener('load', () => burst(10000, 400));
	addEventListener('visibilitychange', () => { if (!document.hidden) burst(4000, 400); });

	burst(12000, 400); // старт
})();