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, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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); // старт
})();