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.
// ==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); // старт
})();