Проигрывать видео только аппаратными кодеками, поддерживаемые вашим устройством. (Перехватывает: canPlayType, isTypeSupported, addSourceBuffer, decodingInfo, VideoDecoder/AudioDecoder.isConfigSupported. Работает во всех фреймах.)
// ==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);
})();