Use Hardware Codecs

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

スクリプトをインストールするには、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         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.1.4.3
// @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
// ==/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'];

// ═══════════════════════════════════════════════════════════════════════════
// 🛠️ ЛОГИ (активны только при DEBUG=true)
// ═══════════════════════════════════════════════════════════════════════════
const log  = (...a) => { if (DEBUG) console.log('[HW]',  ...a); };
const warn = (...a) => { if (DEBUG) 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();
    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(тестируется)
const cachedData = loadCache();
const fromCache  = !!cachedData;
const codecMap   = window[MAP_KEY] || (window[MAP_KEY] = cachedData || new Map());

// ═══════════════════════════════════════════════════════════════════════════
// 📡 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 ? [...codecMap.values()].some(v => v === true) : 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();
                cacheReady = false; anyHWFound = false; testRunning = false;
                log('BC: clear-cache');
                break;
            case 'cache-updated':
                // Перечитываем кэш — следующие вызовы canPlayType/isTypeSupported/
                // addSourceBuffer уже получат точный ответ (решение ПРОБЛЕМ №2,3)
                const fresh = loadCache();
                if (fresh) {
                    fresh.forEach((v, k) => codecMap.set(k, v));
                    anyHWFound = [...codecMap.values()].some(v => v === true);
                    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)) return codecMap.get(codec);
    codecMap.set(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);
    log('Тест:', codec, hw ? '✅ HW' : '❌ SW');

    // Подсказка: если плеер нашёл HW-кодек которого не было в массиве —
    // его стоит добавить в CODECS_TO_TEST для более быстрого старта
    if (hw && !CODECS_TO_TEST.includes(codec)) {
        warn('💡 HW-кодек не в CODECS_TO_TEST, добавьте для ускорения:');
        warn(`   '${codec}',`);
    }

    return hw;
};

// Нормализация AV1: длинные строки → первые 13 символов (базовый профиль)
const normalize = (c) => (typeof c === 'string' && /^av01\./.test(c)) ? c.slice(0, 13) : c;

// ═══════════════════════════════════════════════════════════════════════════
// 📊 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;
    anyHWFound = [...codecMap.values()].some(v => v === true);

    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';
    const m = /codecs=["']?([^"',;\s]+)/.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'; }
        // ПРОБЛЕМА №3 — BroadcastChannel 'cache-updated' решит для следующего вызова
        log('🚫 BLOCK (#3: кэш пуст, не h264, AllowSW=false):', codec);
        return 'block';
    }

    // ── Кодек не в кэше → тестируем ──────────────────────────────────────
    const cached = codecMap.get(codec);
    if (cached === undefined || cached === null) {
        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 ─────────────────────────────────────────────────
    if (cached === true) {
        countSWBlock = 0;
        if (DEBUG) console.log('[HW] ✅ ALLOW (HW):', raw, '→ кэш HW ✓');
        return 'allow';
    }

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

    // HW=0 в кэше — устройство не поддерживает HW
    if (!anyHWFound) {
        countSWBlock = 0;
        if (AllowSW) { log('✅ ALLOW (HW=0, AllowSW=true):', codec); return 'allow'; }
        log('🚫 BLOCK (HW=0, AllowSW=false):', codec);
        return 'block';
    }

    // Есть HW в кэше, но плеер запросил SW — блокируем до порога
    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 = /codecs=["']?([^"',;\s]+)/.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 (cached === undefined || cached === null) {
                    if (!getLock()) {
                        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 установлен');
}

if (typeof AudioDecoder !== 'undefined' && AudioDecoder.isConfigSupported) {
    const origAudioDecoder = AudioDecoder.isConfigSupported;
    AudioDecoder.isConfigSupported = async function (config) {
        // Аудио не фильтруем — пропускаем к оригинальному API
        return origAudioDecoder(config);
    };
    log('Перехватчик AudioDecoder.isConfigSupported установлен (аудио не фильтруется)');
}

// ═══════════════════════════════════════════════════════════════════════════
// 🚀 ИНИЦИАЛИЗАЦИЯ — превентивное заполнение кэша (ветка алгоритма 1.1)
// setTimeout(0): уступаем синхронную очередь i_hate_waiting, но стартуем
// раньше requestIdleCallback. fillCache() асинхронный — рендер не блокирует.
// ═══════════════════════════════════════════════════════════════════════════
setTimeout(() => {
    if (fromCache) {
        log('init: кэш загружен, anyHW:', anyHWFound);
        printSummary();
        return;
    }
    if (!getLock()) {
        log('init: кэш пуст → превентивный fillCache()');
        fillCache();
    } else {
        log('init: кэш пуст, LOCK=true — другая вкладка заполняет');
    }
}, 0);

})();