Use Hardware Codecs

Проигрывать видео только аппаратными кодеками, поддерживаемые вашим устройством. (Перехватывает: canPlayType, isTypeSupported, addSourceBuffer, decodingInfo, VideoDecoder/AudioDecoder.isConfigSupported. Работает во всех фреймах.)

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

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

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==UserScript==
// @name         Use Hardware Codecs
// @name:en      Use Hardware Codecs
// @description  Проигрывать видео только аппаратными кодеками, поддерживаемые вашим устройством. (Перехватывает: canPlayType, isTypeSupported, addSourceBuffer, decodingInfo, VideoDecoder/AudioDecoder.isConfigSupported. Работает во всех фреймах.)
// @description:en Play videos using only the hardware decoders supported by your device. (Intercepts: canPlayType, isTypeSupported, addSourceBuffer, decodingInfo, VideoDecoder/AudioDecoder.isConfigSupported. Runs in every frame.)
// @namespace    http://tampermonkey.net/
// @version      2.3.0
// @author       CY Fung → Qwen → DeepSeek → Claude → You
// @match        *://*/*
// @exclude      *://localhost/*
// @exclude      *://127.0.0.1/*
// @grant        none
// @run-at       document-start
// @all-frames   true
// @inject-into  page
// @license      MIT
// @compatible   firefox 38+ Violentmonkey
// @compatible   firefox 38+ Tampermonkey
// @compatible   firefox 38+ GreaseMonkey
// @compatible   chrome  54+ Violentmonkey
// @compatible   chrome  54+ Tampermonkey
// @compatible   chrome  54+ ScriptCat
// @compatible   safari 15.4+ Stay
// @compatible   edge 79+ Tampermonkey
// @compatible   opera 41+ Tampermonkey
// @compatible   android Via Browser
// ==/UserScript==

(function () {
'use strict';

// ═══════════════════════════════════════════════════════════════════════════
// ⚙️ НАСТРОЙКИ ПОЛЬЗОВАТЕЛЯ
// ═══════════════════════════════════════════════════════════════════════════
const DEBUG      = false; // true = подробные логи в консоль (F12)
const ClearCache = false; // true = очистить кэш при следующей загрузке
                          //        (сбрасывается автоматически, без дублирования)
const AllowSW    = false; // true = разрешить SW-кодек если HW недоступен
                          //        false = строгий режим (только HW)

// Порог SW-запросов до принудительного разрешения — защита от "чёрного экрана"
// (ПРОБЛЕМА №1). Если плеер N раз подряд прислал только SW-кодеки (при
// наличии HW в кэше) — значит он не знает HW-вариантов. При достижении
// порога разрешаем последний SW-запрос (AllowSW=true) или блокируем (false).
// Рекомендуемые значения: 4–8. Меньше → ложные разрешения SW. Больше →
// плеер может прекратить запросы раньше порога → "чёрный экран".
const SW_BLOCK_THRESHOLD = 6;

// Исключённые домены — скрипт полностью пропускает эти сайты
// Rutube.ru видеохостинг добавлен, т.к Rutube запускает свой плеер в Web Worker или WASM-контекст. Web Worker — отдельный JavaScript-поток, в который нельзя инжектировать ни userscript, ни extension content script. Все наши перехватчики вешаются на window главного потока — Worker их не видит.
const EXCLUDED_DOMAINS = ['google.com', 'github.com', 'rutube.ru', 'stackoverflow.com'];

// ═══════════════════════════════════════════════════════════════════════════
// 🔧 ВСПОМОГАТЕЛЬНЫЕ УТИЛИТЫ (вынесены на уровень модуля для оптимизации)
// ═══════════════════════════════════════════════════════════════════════════

// RE_CODEC: компилируется один раз — checkCodec вызывается на КАЖДЫЙ запрос
// плеера (canPlayType, isTypeSupported, addSourceBuffer, decodingInfo).
// Инлайн-литерал /regex/ тоже кэшируется V8, но явный вынос гарантирует
// единственную компиляцию во всех движках (SpiderMonkey, JavaScriptCore).
const RE_CODEC = /codecs=["']?([^"',;\s]+)/;

// hasAnyHW: заменяет [...map.values()].some(v => v === true).
// Спред-оператор создаёт временный массив → лишняя аллокация → давление на GC.
// Ручной for..of останавливается на первом HW без создания промежуточных объектов.
const hasAnyHW = (map) => { for (const v of map.values()) if (v === true) return true; return false; };

// ═══════════════════════════════════════════════════════════════════════════
// 🛠️ ЛОГИ
// При DEBUG=false log/warn — пустые функции. JIT убирает их вызовы полностью.
// ═══════════════════════════════════════════════════════════════════════════
const log  = DEBUG ? (...a) => console.log('[HW]',  ...a) : () => {};
const warn = DEBUG ? (...a) => console.warn('[HW]', ...a) : () => {};

// ═══════════════════════════════════════════════════════════════════════════
// 📱 ОПРЕДЕЛЕНИЕ ПЛАТФОРМЫ
// ═══════════════════════════════════════════════════════════════════════════
const isMobile = /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent)
    || (navigator.maxTouchPoints > 1 && window.innerWidth < 1024);
log('Платформа:', isMobile ? 'Mobile' : 'Desktop');

// ═══════════════════════════════════════════════════════════════════════════
// 📦 МАССИВ КОДЕКОВ ДЛЯ ТЕСТИРОВАНИЯ
// Порядок: лучшее сжатие первым (AV1 → HEVC → VP9 → H.264)
// Внутри группы: по частоте встречаемости на реальных сайтах
// ═══════════════════════════════════════════════════════════════════════════
const CODECS_TO_TEST = [
    // TIER 1: AV1 — лучшее сжатие (GPU: RX 6000+, RTX 3000+, Intel Arc, Apple M1+)
    'av01.0.05M.08', 'av01.0.08M.08', 'av01.0.04M.08',
    'av01.0.05M.10', 'av01.0.08M.10', 'av01.0.04M.10',
    // TIER 2: H.265/HEVC — хорошее сжатие (GPU: большинство с 2016+)
    'hvc1.1.6.L123.00', 'hev1.1.6.L93.B0', 'hvc1.1.6.L153.B0', 'hvc1.2.4.L153.B0',
    // TIER 3: VP9 — хорошее сжатие, широкая поддержка
    'vp09.00.10.08', 'vp09.00.51.08', 'vp09.02.51.10.01.09.16.09.00', 'vp8',
    // TIER 4: H.264 — базовый, используется как safe fallback при пустом кэше
    'avc1.42001E', 'avc1.42001F', 'avc1.4d401f', 'avc1.4d401e', 'avc1.640028', 'avc1.64002a',
    // TIER 5: Аудио кодеки
    'mp4a.40.2', 'opus', 'mp4a.40.5', 'ac-3', 'ec-3',
    // TIER 6: Дополнительные варианты (YouTube, Rutube, Twitch часто запрашивают)
    'vp9', 'vp09.00.51.08.01.01.01.01.00', 'av01.0.01M.08', 'av01.0.00M.08',
];

// ═══════════════════════════════════════════════════════════════════════════
// 🛡️ SAFE FALLBACK — разрешаются пока кэш пуст/заполняется
// Только H.264 (98% устройств поддерживают HW) + основные аудио
// ═══════════════════════════════════════════════════════════════════════════
const SAFE_FALLBACK_PREFIXES = isMobile
    ? ['avc1.4200', 'mp4a.40.2', 'opus']                            // Mobile
    : ['avc1.4200', 'avc1.4d40', 'avc1.6400', 'mp4a.40.2', 'opus'];// Desktop
log('Safe Fallback:', SAFE_FALLBACK_PREFIXES);

// ═══════════════════════════════════════════════════════════════════════════
// 🔧 РАННИЕ ПРОВЕРКИ
// ═══════════════════════════════════════════════════════════════════════════
if (EXCLUDED_DOMAINS.some(d => location.hostname.includes(d))) return;

if (typeof VideoDecoder !== 'function' ||
    typeof VideoDecoder.isConfigSupported !== 'function') {
    warn('VideoDecoder API недоступен — скрипт отключён');
    return;
}

// ═══════════════════════════════════════════════════════════════════════════
// 💾 КЭШ — localStorage, постоянный (без TTL)
// Аппаратные возможности устройства не меняются → кэш вечный.
// Сброс только по ClearCache=true через BroadcastChannel.
// ═══════════════════════════════════════════════════════════════════════════
const LS_KEY  = 'hw_codecs_v1';  // данные кэша {codec: true/false}
const LS_LOCK = 'hw_codecs_lck'; // межвкладочный LOCK_RUN
const MAP_KEY = '__hw_map__';    // window-ключ: общая карта для вкладок домена

const getLock = ()    => localStorage.getItem(LS_LOCK) === '1';
const setLock = (val) => { try { localStorage.setItem(LS_LOCK, val ? '1' : '0'); } catch {} };

const clearCacheStorage = () => {
    try { localStorage.removeItem(LS_KEY); localStorage.removeItem(LS_LOCK); } catch {}
    if (window[MAP_KEY]) window[MAP_KEY].clear();
    testingSet.clear();
    log('Кэш очищен');
};

const loadCache = () => {
    try {
        const raw = localStorage.getItem(LS_KEY);
        return raw ? new Map(Object.entries(JSON.parse(raw))) : null;
    } catch { return null; }
};

const saveCache = () => {
    try { localStorage.setItem(LS_KEY, JSON.stringify(Object.fromEntries(codecMap))); }
    catch (e) { warn('saveCache:', e); }
};

// Единая карта кодеков: codec → true(HW) | false(SW)
// null-маркер "тестируется" убран в v2.2.0 — вместо него testingSet ниже.
const cachedData = loadCache();
const fromCache  = !!cachedData;
const codecMap   = window[MAP_KEY] || (window[MAP_KEY] = cachedData || new Map());

// testingSet — Set кодеков которые сейчас тестируются в ЭТОЙ вкладке.
// Заменяет маркер codecMap.set(codec, null) из предыдущих версий.
// Преимущества:
//   • codecMap содержит ТОЛЬКО финальные результаты (true/false) — нет "мусора"
//   • hasAnyHW работает быстрее (не встречает null при переборе)
//   • Нет "слепых зон": состояние кодека всегда явное:
//       codecMap.has(codec)    = false  → не тестировался вообще
//       testingSet.has(codec)  = true   → тестируется прямо сейчас
//       codecMap.get(codec)    = true   → HW (финально)
//       codecMap.get(codec)    = false  → SW (финально)
const testingSet = new Set();

// ═══════════════════════════════════════════════════════════════════════════
// 📡 BROADCASTCHANNEL — межвкладочная синхронизация
// 'clear-cache'   → все вкладки очищают кэш (без дублирования)
// 'cache-updated' → другая вкладка заполнила кэш → перечитать
//                   Решает ПРОБЛЕМЫ №2 и №3: следующий вызов плеера
//                   уже получит точный ответ из свежего кэша
// ═══════════════════════════════════════════════════════════════════════════
let bc = null;
try { bc = new BroadcastChannel('hw_codecs_bc_v1'); } catch {}

let cacheReady  = fromCache;
let anyHWFound  = fromCache ? hasAnyHW(codecMap) : false;

// testRunning — in-memory флаг ЭТОЙ вкладки.
// Закрывает race condition внутри одной вкладки мгновенно (без обращения к
// localStorage), что устраняет проблему "5 параллельных fillCache" из-за
// ~1-5мс окна между getLock() и setLock() в одной вкладке.
// Для cross-tab race результат идемпотентный — обе вкладки тестируют одно
// устройство и получат одинаковые данные, поэтому дублирование безвредно.
let testRunning = false;

// Счётчик последовательных SW-блокировок (ПРОБЛЕМА №1).
// Сбрасывается только при: HW-разрешении | достижении порога | HW=0 в кэше.
let countSWBlock = 0;

if (bc) {
    bc.onmessage = ({ data }) => {
        switch (data?.type) {
            case 'clear-cache':
                clearCacheStorage(); // уже чистит testingSet
                cacheReady = false; anyHWFound = false; testRunning = false;
                log('BC: clear-cache');
                break;
            case 'cache-updated':
                // Перечитываем кэш — следующие вызовы получат точный ответ
                const fresh = loadCache();
                if (fresh) {
                    fresh.forEach((v, k) => codecMap.set(k, v));
                    // Кодеки из другой вкладки уже финальные — очищаем testingSet
                    // (там могли остаться кодеки которые другая вкладка уже протестила)
                    testingSet.clear();
                    anyHWFound = hasAnyHW(codecMap);
                    cacheReady = true;
                    log('BC: cache-updated → anyHW:', anyHWFound);
                }
                break;
        }
    };
}

// ═══════════════════════════════════════════════════════════════════════════
// ⚡ ClearCache=true → однократная очистка через BroadcastChannel
// sessionStorage (только эта вкладка) предотвращает повтор при перезагрузке
// ═══════════════════════════════════════════════════════════════════════════
if (ClearCache && !sessionStorage.getItem('hw_clear_sent')) {
    sessionStorage.setItem('hw_clear_sent', '1');
    clearCacheStorage();
    cacheReady = false; anyHWFound = false;
    if (bc) bc.postMessage({ type: 'clear-cache' });
    log('ClearCache=true → кэш очищен, команда разослана');
}

// ═══════════════════════════════════════════════════════════════════════════
// 🧪 ТЕСТ ОДНОГО КОДЕКА
// VideoDecoder.isConfigSupported(prefer-hardware) → supported=true = HW на GPU
// ═══════════════════════════════════════════════════════════════════════════
const testCodec = async (codec) => {
    // Пропускаем если: нет кодека | уже в кэше (финальный) | уже тестируется
    if (!codec || codecMap.has(codec) || testingSet.has(codec)) return codecMap.get(codec);
    testingSet.add(codec); // помечаем "тестируется" — не в кэше, не null

    const cfg = {
        codec, hardwareAcceleration: 'prefer-hardware',
        width: 1920, height: 1080, bitrate: 8_000_000,
        bitrateMode: 'variable', framerate: 30,
        sampleRate: 48000, numberOfChannels: 2,
    };
    let hw = false;
    try {
        const [vr, ar] = await Promise.all([
            VideoDecoder.isConfigSupported(cfg).catch(() => ({ supported: false })),
            (typeof AudioDecoder !== 'undefined'
                ? AudioDecoder.isConfigSupported(cfg)
                : Promise.resolve({ supported: false })
            ).catch(() => ({ supported: false })),
        ]);
        hw = vr.supported === true || ar.supported === true;
    } catch (e) { warn('testCodec:', codec, e); }

    codecMap.set(codec, hw);   // финальный результат → в кэш
    testingSet.delete(codec);  // убираем маркер тестирования

    log('Тест:', codec, hw ? '✅ HW' : '❌ SW');

    if (hw && !CODECS_TO_TEST.includes(codec)) {
        warn('💡 HW-кодек не в CODECS_TO_TEST, добавьте для ускорения:');
        warn(`   '${codec}',`);
    }

    return hw;
};

// normalize: startsWith быстрее regex-теста на строках (нет конечного автомата)
const normalize = (c) => (typeof c === 'string' && c.startsWith('av01.')) ? c.slice(0, 13) : c;

// ═══════════════════════════════════════════════════════════════════════════
// 📊 DEBUG: СВОДКА КОДЕКОВ
// ═══════════════════════════════════════════════════════════════════════════
// printSummary: тело обёрнуто в if(DEBUG) — в релизе функция не вызывается вовсе,
// нет даже накладных расходов на вызов кадра стека.
const printSummary = () => {
    if (!DEBUG) return;
    const vHW=[], vSW=[], aHW=[], aSW=[];
    const isVid = c => /^(avc1|avc3|hvc1|hev1|vp09|vp8|av01|vp9)/.test(c);
    codecMap.forEach((hw, c) => {
        const arr = hw ? (isVid(c) ? vHW : aHW) : (isVid(c) ? vSW : aSW);
        arr.push(c);
    });
    console.groupCollapsed('[HW] 📊 Кодеки | HW=' + (vHW.length+aHW.length) + ' SW=' + (vSW.length+aSW.length));
    console.log('%c✅ Видео HW:', 'color:green;font-weight:bold', vHW);
    console.log('%c❌ Видео SW:', 'color:red;font-weight:bold',   vSW);
    console.log('%c✅ Аудио HW:', 'color:green;font-weight:bold', aHW);
    console.log('%c❌ Аудио SW:', 'color:red;font-weight:bold',   aSW);
    console.groupEnd();
};

// ═══════════════════════════════════════════════════════════════════════════
// 🏃 ЗАПОЛНЕНИЕ КЭША — тест всего массива параллельно
// Пока идёт тест → плеер получает H.264 safe fallback.
// После завершения → уведомление всех вкладок через BroadcastChannel.
// ═══════════════════════════════════════════════════════════════════════════
const fillCache = async () => {
    // Двойная защита от параллельного запуска:
    // 1. testRunning — мгновенная in-memory проверка (одна вкладка)
    // 2. getLock()   — cross-tab проверка через localStorage
    if (testRunning || getLock()) return;
    testRunning = true;
    setLock(true);
    log('fillCache: старт');

    await Promise.all(CODECS_TO_TEST.map(testCodec));

    setLock(false);
    testRunning = false;
    testingSet.clear(); // все тесты завершены — Set должен быть пуст, но чистим явно
    anyHWFound = hasAnyHW(codecMap);

    if (!anyHWFound) {
        // Массив не содержит HW-кодеков устройства, но скрипт продолжает работу:
        // плеер может запросить кодек которого нет в массиве — он будет
        // протестирован и добавлен в кэш через ветку TEST_NEW_CODEC
        warn('⚠️ HW=0 в CODECS_TO_TEST. Скрипт активен — ждём запросов плеера.');
        warn('💡 Если плеер найдёт HW-кодек — он появится в логе с подсказкой.');
    } else {
        saveCache();
        log('fillCache: готово, anyHW:', anyHWFound);
        printSummary();
    }
    cacheReady = true;

    // Уведомляем другие вкладки → решение ПРОБЛЕМ №2 и №3
    if (bc) bc.postMessage({ type: 'cache-updated' });
};

// ═══════════════════════════════════════════════════════════════════════════
// 🔍 ОБЩАЯ ЛОГИКА ПРОВЕРКИ КОДЕКА
// Возвращает: 'allow' | 'block' | 'fallback'
// Используется всеми тремя перехватчиками: canPlayType, isTypeSupported,
// addSourceBuffer — единая точка принятия решения (DRY)
// ═══════════════════════════════════════════════════════════════════════════
const checkCodec = (mimeType) => {
    if (typeof mimeType !== 'string') return 'allow';
    // indexOf — нативная операция без аллокаций, в разы быстрее regex.
    // Большинство вызовов canPlayType содержат типы без codecs= (например
    // 'video/mp4' без параметров) — они выходят здесь не достигая RE_CODEC.
	
    if (mimeType.indexOf('codecs=') === -1) return 'allow';
    const m = RE_CODEC.exec(mimeType);
    if (!m) return 'allow';

    const raw   = m[1];
    const codec = normalize(raw);

    // ── Кэш не готов (заполняется или пуст) ──────────────────────────────
    if (!cacheReady) {
        if (!getLock()) fillCache();

        const isH264 = codec.startsWith('avc1') || codec.startsWith('avc3');
        if (isH264)  { log('✅ ALLOW (h264 fallback, кэш пуст):', codec); return 'allow'; }
        if (AllowSW) { log('✅ ALLOW (AllowSW=true, кэш пуст):', codec);  return 'allow'; }
        log('🚫 BLOCK (#3: кэш пуст, не h264, AllowSW=false):', codec);
        return 'block';
    }

    // ── Кодек тестируется прямо сейчас (был запущен testCodec асинхронно) ─
    // Разрешаем временно — результат придёт через bc 'cache-updated'
    if (testingSet.has(codec)) {
        log('✅ ALLOW (тестируется сейчас):', codec);
        return 'allow';
    }

    // ── Кодека нет в кэше и не тестируется → запускаем тест ──────────────
    if (!codecMap.has(codec)) {
        if (getLock()) {
            // ПРОБЛЕМА №2 — другой процесс тестирует, разрешаем временно
            log('✅ ALLOW (#2: не в кэше, LOCK=true):', codec);
            return 'allow';
        }
        // Тест нового кодека запрошенного плеером
        testCodec(codec).then(() => { saveCache(); if (bc) bc.postMessage({ type: 'cache-updated' }); });
        log('✅ ALLOW (тест запущен):', codec);
        return 'allow';
    }

    // ── Кодек в кэше: HW ─────────────────────────────────────────────────
    const cached = codecMap.get(codec);
    if (cached === true) {
        countSWBlock = 0;
        if (DEBUG) console.log('[HW] ✅ ALLOW (HW):', raw, '→ кэш HW ✓');
        return 'allow';
    }

    // ── Кодек в кэше: SW (cached === false) ──────────────────────────────
    countSWBlock++;
    log('❌ SW запрос #' + countSWBlock + ':', codec);

    if (!anyHWFound) {
        countSWBlock = 0;
        if (AllowSW) { log('✅ ALLOW (HW=0, AllowSW=true):', codec); return 'allow'; }
        log('🚫 BLOCK (HW=0, AllowSW=false):', codec);
        return 'block';
    }

    if (countSWBlock < SW_BLOCK_THRESHOLD) {
        log('🚫 BLOCK (SW, ' + countSWBlock + '/' + SW_BLOCK_THRESHOLD + '):', codec);
        return 'block';
    }

    // Порог достигнут — ПРОБЛЕМА №1
    countSWBlock = 0;
    if (AllowSW) { warn('⚠️ Порог SW (#1), AllowSW=true → разрешаем:', codec); return 'allow'; }
    warn('⚠️ Порог SW (#1), AllowSW=false → блокируем:', codec);
    return 'block';
};

// ═══════════════════════════════════════════════════════════════════════════
// 🔗 ПЕРЕХВАТЧИКИ
// ─────────────────────────────────────────────────────────────────────────
// 1. canPlayType         — стандартный путь (YouTube, большинство сайтов)
// 2. isTypeSupported     — MSE-проверка поддержки (YouTube, Vimeo)
// 3. addSourceBuffer     — MSE создание буфера (Twitch, Vimeo и др.)
// 4. decodingInfo        — MediaCapabilities API (Rutube и современные плееры)
//    Rutube использует именно decodingInfo для выбора кодека и НЕ вызывает
//    canPlayType/isTypeSupported. Без этого перехватчика скрипт на Rutube
//    не работает — плеер выбирает кодек без нашего участия.
//    decodingInfo возвращает Promise → мы можем вернуть {supported:false}
//    для SW-кодеков асинхронно, без SW_BLOCK_THRESHOLD.
// ═══════════════════════════════════════════════════════════════════════════

// 1 + 2: canPlayType / isTypeSupported
const makeReturnInterceptor = (origFn, emptyVal) => function (type) {
    if (typeof type !== 'string' || !type.startsWith('video/'))
        return origFn.apply(this, arguments);
    const decision = checkCodec(type);
    return decision === 'block' ? emptyVal : origFn.apply(this, arguments);
};

const vProto = HTMLVideoElement?.prototype;
if (vProto?.canPlayType) {
    vProto.canPlayType = makeReturnInterceptor(vProto.canPlayType, '');
    log('Перехватчик canPlayType установлен');
}
const mse = window.MediaSource;
if (mse?.isTypeSupported) {
    mse.isTypeSupported = makeReturnInterceptor(mse.isTypeSupported, false);
    log('Перехватчик isTypeSupported установлен');
}

// 3: addSourceBuffer — MSE создание буфера (Twitch, Vimeo и другие)
// При блокировке бросаем NotSupportedError — плеер воспринимает это как
// "кодек не поддерживается" и переходит к следующему варианту.
if (mse?.prototype?.addSourceBuffer) {
    const origAddSourceBuffer = mse.prototype.addSourceBuffer;
    mse.prototype.addSourceBuffer = function (mimeType) {
        if (typeof mimeType === 'string' && mimeType.startsWith('video/')) {
            const decision = checkCodec(mimeType);
            if (decision === 'block') {
                log('🚫 addSourceBuffer BLOCKED:', mimeType);
                throw new DOMException(
                    'HW codec required. SW codec blocked by Use Hardware Codecs script.',
                    'NotSupportedError'
                );
            }
            log('✅ addSourceBuffer ALLOWED:', mimeType);
        }
        return origAddSourceBuffer.call(this, mimeType);
    };
    log('Перехватчик addSourceBuffer установлен');
}

// 4: mediaCapabilities.decodingInfo — используется Rutube и современными плеерами
// Rutube НЕ вызывает canPlayType/isTypeSupported — он запрашивает decodingInfo
// для каждого кодека и выбирает лучший по результату. Перехватываем Promise:
// SW-кодек → возвращаем {supported:false, smooth:false, powerEfficient:false}
// HW-кодек → пропускаем к оригинальному API (браузер отвечает сам)
// Кэш не готов → пропускаем (плеер получит настоящий ответ браузера)
// Нет нужды в SW_BLOCK_THRESHOLD — Promise позволяет точно ответить на каждый
// запрос, плеер сам выберет лучший из разрешённых кодеков.
if (navigator.mediaCapabilities?.decodingInfo) {
    const origDecodingInfo = navigator.mediaCapabilities.decodingInfo.bind(navigator.mediaCapabilities);
    navigator.mediaCapabilities.decodingInfo = async function (config) {
        // Извлекаем кодек из конфига video (если есть)
        const contentType = config?.video?.contentType;
        if (typeof contentType === 'string' && contentType.startsWith('video/')) {
            // Если кэш не готов — не вмешиваемся, браузер ответит сам
            if (!cacheReady) {
                if (!getLock()) fillCache();
                return origDecodingInfo(config);
            }

            const m = RE_CODEC.exec(contentType);
            if (m) {
                const codec  = normalize(m[1]);
                const cached = codecMap.get(codec);

                if (cached === false) {
                    // SW-кодек в кэше.
                    // Если HW=0 (устройство не поддерживает ни одного HW) — решает AllowSW.
                    // Если HW есть в кэше — блокируем SW, плеер перейдёт к HW-варианту.
                    if (!anyHWFound) {
                        if (AllowSW) {
                            log('✅ decodingInfo ALLOW (HW=0, AllowSW=true):', codec);
                            return origDecodingInfo(config);
                        }
                        log('🚫 decodingInfo BLOCK (HW=0, AllowSW=false):', codec);
                        return { supported: false, smooth: false, powerEfficient: false };
                    }
                    if (AllowSW) {
                        log('✅ decodingInfo ALLOW (SW, AllowSW=true):', codec);
                        return origDecodingInfo(config);
                    }
                    log('🚫 decodingInfo BLOCK (SW):', codec);
                    return { supported: false, smooth: false, powerEfficient: false };
                }

                if (cached === true) {
                    log('✅ decodingInfo ALLOWED (HW):', codec);
                    return origDecodingInfo(config);
                }

                // Кодека нет в кэше — тестируем и пропускаем пока
                if (!codecMap.has(codec) || testingSet.has(codec)) {
                    if (!getLock() && !testingSet.has(codec)) {
                        testCodec(codec).then(() => {
                            saveCache();
                            if (bc) bc.postMessage({ type: 'cache-updated' });
                        });
                    }
                    log('✅ decodingInfo ALLOW (тест запущен/тестируется):', codec);
                    return origDecodingInfo(config);
                }
            }
        }
        return origDecodingInfo(config);
    };
    log('Перехватчик decodingInfo установлен');
}

// ═══════════════════════════════════════════════════════════════════════════
// 5+6: VideoDecoder.isConfigSupported / AudioDecoder.isConfigSupported
// Низкоуровневый перехват — последний рубеж.
// Некоторые плееры (особенно в cross-origin iframe) вызывают этот API напрямую,
// минуя canPlayType, isTypeSupported, addSourceBuffer и decodingInfo.
//
// ⚠️ ВАЖНО про Rutube и Web Workers:
// Rutube запускает плеер в Web Worker. Workers — отдельный JS-поток,
// в который нельзя инжектировать userscript или extension content script.
// Даже этот перехватчик не достигает Worker-контекста.
// Однако Rutube самостоятельно выбирает HW-кодеки (avc1.640028) —
// скрипт на его работу не влияет ни позитивно, ни негативно.
//
// AudioDecoder: строка audio/mp4 не проходит через checkCodec (только video/),
// поэтому аудио-кодеки пропускаем всегда — фильтрация только видео.
// ═══════════════════════════════════════════════════════════════════════════
if (typeof VideoDecoder !== 'undefined' && VideoDecoder.isConfigSupported) {
    const origVideoDecoder = VideoDecoder.isConfigSupported;
    VideoDecoder.isConfigSupported = async function (config) {
        const codec = config?.codec;
        if (codec && typeof codec === 'string') {
            const fakeMime = 'video/mp4; codecs="' + codec + '"';
            if (checkCodec(fakeMime) === 'block') {
                log('🚫 VideoDecoder.isConfigSupported BLOCKED (SW):', codec);
                return { supported: false };
            }
            log('✅ VideoDecoder.isConfigSupported ALLOW:', codec);
        }
        return origVideoDecoder(config);
    };
    log('Перехватчик VideoDecoder.isConfigSupported установлен');
}

// AudioDecoder.isConfigSupported не перехватываем: аудио-кодеки не фильтруются
// по HW/SW (логика SW_BLOCK_THRESHOLD рассчитана только на видео), поэтому
// проксирующий перехватчик давал только лишний Promise-overhead без пользы.

// ═══════════════════════════════════════════════════════════════════════════
// 🚀 ИНИЦИАЛИЗАЦИЯ
//
// v2.3.0: превентивный fillCache при старте УДАЛЁН.
//
// Причина: fillCache (30+ вызовов VideoDecoder.isConfigSupported) запускался
// на КАЖДОЙ странице КАЖДОГО домена даже если там нет видео совсем.
// Это 50-200мс CPU в idle на каждой загрузке — бессмысленная трата АКБ.
//
// Кэш localStorage изолирован по домену (same-origin) — результаты с
// youtube.com недоступны на vimeo.com и наоборот. Поэтому превентивное
// заполнение на "первом визите к домену" всё равно не помогает другим
// сайтам, а только тратит ресурсы.
//
// Теперь: fillCache запускается ТОЛЬКО из checkCodec при первом реальном
// запросе кодека от видеоплеера. На сайте без видео скрипт висит молча —
// перехватчики установлены, но ничего не делают до первого вызова.
//
// Единственное что делаем при старте (если кэш уже есть): DEBUG-сводка.
// ═══════════════════════════════════════════════════════════════════════════
if (DEBUG && fromCache) {
    // Откладываем в idle — не мешаем рендеру первого кадра
    (window.requestIdleCallback || setTimeout)(() => {
        log('init: кэш загружен, anyHW:', anyHWFound);
        printSummary();
    }, { timeout: 2000 });
}

})();