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.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 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); // старт
})();