/
dalek
/
Video-Controller
Обзор
Документация
Войти
/
dalek
/
Video-Controller
Код
Запросы
0
Пакеты
0
Релизы
2
Аналитика
Безопасность
master
video-controller.js
523 строки
22 KB
Николай Киреев
update video-controller.js
28 мар 2025, 13:02
28 мар 2025, 13:02
3bd4ba3
Код
Авторство
О чём код?
/** * VIDEO CONTROLLER v1.4 * Скрипт для управления видео элементами на странице: * - Контроль атрибутов и свойств video * - Блокировка нежелательных функций (fullscreen, PiP и др.) * - Обработка динамически добавляемых видео * - Защита от горячих клавиш * - Поддержка мультиязычности (ru/en) */ /** * КОНФИГУРАЦИОННЫЕ ПАРАМЕТРЫ * Объект содержит все настраиваемые параметры скрипта. * Можно менять перед запуском скрипта. */ const CONFIG = { // Язык сообщений (ru/en) language: 'ru', // Целевой элемент для наблюдения MutationObserver // null - весь документ // '#id' - конкретный элемент по id // '.class' - конкретный элемент по class targetElement: 'body', // Настройки видео элементов videoSettings: { // Показывать элементы управления (панель управления видео) controls: true, // Зацикливать воспроизведение (видео повторяется автоматически) loop: false, // Без звука по умолчанию (необходимо для автовоспроизведения в некоторых браузерах) muted: true, // Автоматическое воспроизведение (может не работать без muted) autoplay: false, // Воспроизведение внутри страницы (для iOS - без перехода в полноэкранный режим) playsInline: true, // Запретить режим "картинка в картинке" (PiP) disablePictureInPicture: true, // Предзагрузка видео (auto - браузер сам решает, metadata - только метаданные, none - не загружать) preload: 'auto', // Ограничения элементов управления (какие кнопки скрыть) controlsList: 'noplaybackrate nodownload nofullscreen', // Фиксировать скорость воспроизведения (не давать пользователю менять) fixPlaybackRate: true, // Фиксированная скорость (0.5 - замедленная, 1 - нормальная, 2 - ускоренная) playbackRate: 1 }, // Стили для видео элементов (применяются через style attribute) videoStyles: { // Ширина видео (можно задать в px, %, vw и др.) width: '100%', // Блочное отображение display: 'block', // Курсор в виде указателя при наведении cursor: 'pointer' }, // Настройки отладки (логирование в консоль) debug: { // Логировать инициализацию скрипта logInitialization: false, // Логировать найденные видео элементы logVideosFound: true, // Логировать изменения DOM (может быть много сообщений) logMutations: false, // Логировать блокировку горячих клавиш logHotkeys: false }, // Текстовые сообщения (мультиязычные) messages: { ru: { initialization: { start: '[Video Controller] Инициализация скрипта', config: '[Video Controller] Конфигурация:', complete: '[Video Controller] Инициализация завершена' }, videos: { processing: '[Video Controller] Обработка видео:', foundCount: '[Video Controller] Найдено {count} видео элементов в {target}', foundCountDocument: 'документе', targetNotFound: '[Video Controller] Элементы {target} не найдены', playbackRateSet: '[Video Controller] Установлена скорость: {rate}x', playbackRateReset: '[Video Controller] Сброс скорости до {rate}x', newVideoDetected: '[Video Controller] Обнаружено новое видео', nestedVideoDetected: '[Video Controller] Обнаружено видео внутри добавленного элемента', mutationObserverWarning: '[Video Controller] Не удалось найти элементы для наблюдения: {target}' }, errors: { srcError: '[Video Controller] Ошибка установки src:', autoplayBlocked: '[Video Controller] Автовоспроизведение заблокировано:', videoError: '[Video Controller] Ошибка видео:', videoErrorNoDetails: '[Video Controller] Произошла ошибка видео (без details)' }, blockers: { contextMenu: '[Video Controller] Блокировано контекстное меню', ctrlKey: '[Video Controller] Блокировка Ctrl+{key}', f12Key: '[Video Controller] Блокировка F12' }, mutationObserverCreate: '[Video Controller] Мутации DOM:', mutationObserverStart: '[Video Controller] Наблюдение за изменениями в', getMessage: { path: '[Video Controller] Сообщение не найдено:', lang: 'для языка' } }, en: { initialization: { start: '[Video Controller] Script initialization', config: '[Video Controller] Configuration:', complete: '[Video Controller] Initialization complete' }, videos: { processing: '[Video Controller] Processing video:', foundCount: '[Video Controller] Found {count} video elements in {target}', foundCountDocument: 'document', targetNotFound: '[Video Controller] Target elements {target} not found', playbackRateSet: '[Video Controller] Playback rate set to: {rate}x', playbackRateReset: '[Video Controller] Resetting playback rate to {rate}x', newVideoDetected: '[Video Controller] New video detected', nestedVideoDetected: '[Video Controller] Video detected inside added element', mutationObserverWarning: '[Video Controller] Failed to find elements to observe: {target}' }, errors: { srcError: '[Video Controller] Error setting src:', autoplayBlocked: '[Video Controller] Autoplay blocked:', videoError: '[Video Controller] Video error:', videoErrorNoDetails: '[Video Controller] Video error occurred (no details)' }, blockers: { contextMenu: '[Video Controller] Context menu blocked', ctrlKey: '[Video Controller] Blocking Ctrl+{key}', f12Key: '[Video Controller] Blocking F12' }, mutationObserverCreate: '[Video Controller] DOM mutations:', mutationObserverStart: '[Video Controller] Observing changes in', getMessage: { path: '[Video Controller] Message not found:', lang: 'for language' } } } }; /** * Вспомогательная функция для получения локализованного сообщения * @param {string} path - Путь к сообщению (например, 'initialization.start') * @param {object} [params] - Параметры для замены в сообщении * @returns {string} - Локализованное сообщение */ function getMessage(path, params = {}) { const lang = CONFIG.language || 'ru'; const parts = path.split('.'); let message = CONFIG.messages[lang]; for (const part of parts) { message = message?.[part]; if (message === undefined) { console.warn(`${getMessage('getMessage.path')} ${path} ${getMessage('getMessage.lang')} ${lang}`); return path; // Возвращаем путь как fallback } } // Заменяем параметры в сообщении return Object.entries(params).reduce((msg, [key, value]) => msg.replace(new RegExp(`{${key}}`, 'g'), value), message); } /** * Стили для скрытия кнопки полноэкранного режима * Создаем элемент style и добавляем CSS правила для скрытия кнопки fullscreen в разных браузерах (webkit, moz, ms) */ const fullscreenButtonStyle = document.createElement('style'); fullscreenButtonStyle.textContent = ` ${CONFIG.targetElement || ''} video::-webkit-media-controls-fullscreen-button { display: none !important; } ${CONFIG.targetElement || ''} video::-moz-media-controls-fullscreen-button { display: none !important; } ${CONFIG.targetElement || ''} video::-ms-fullscreen-button { display: none !important; } `; document.body.appendChild(fullscreenButtonStyle); /** * Основная функция инициализации * Запускает весь процесс настройки видео элементов */ function initVideoController() { // Логирование начала инициализации if (CONFIG.debug.logInitialization) { console.log(getMessage('initialization.start')); console.log(getMessage('initialization.config'), JSON.parse(JSON.stringify(CONFIG))); } // Проверяем готовность DOM (загружена ли страница полностью) if (document.readyState === 'loading') { // Если DOM еще загружается, ждем события DOMContentLoaded document.addEventListener('DOMContentLoaded', () => { // Обработка всех существующих видео processAllVideos(); // Настройка наблюдения за новыми видео setupMutationObserver(); // Включение блокировки горячих клавиш setupHotkeyBlocker(); }); } else { // Если DOM уже загружен, запускаем сразу // Обработка всех существующих видео processAllVideos(); // Настройка наблюдения за новыми видео setupMutationObserver(); // Включение блокировки горячих клавиш setupHotkeyBlocker(); } // Логирование завершения инициализации if (CONFIG.debug.logInitialization) { console.log(getMessage('initialization.complete')); } } /** * Обрабатывает все существующие видео на странице * Ищет все видео элементы в указанной области (или во всем документе) и применяет к ним настройки */ function processAllVideos() { // Выбираем элементы, в которых будем искать видео // Если targetElement указан, ищем только в нем, иначе во всем документе const baseElements = CONFIG.targetElement ? document.querySelectorAll(CONFIG.targetElement) : [document]; // Если целевые элементы не найдены - предупреждение if (baseElements.length === 0) { console.warn(getMessage('videos.targetNotFound', { target: CONFIG.targetElement })); return; } // Счетчик найденных видео let totalVideos = 0; // Перебираем все базовые элементы baseElements.forEach(baseElement => { // Ищем все видео внутри текущего элемента const videos = baseElement.querySelectorAll('video'); totalVideos += videos.length; // Увеличиваем счетчик // Применяем настройки к каждому видео videos.forEach(video => { if (CONFIG.debug.logVideosFound) { console.log(getMessage('videos.processing'), video); } applyVideoSettings(video); }); }); // Логирование количества найденных видео if (CONFIG.debug.logVideosFound) { console.log(getMessage('videos.foundCount', { count: totalVideos, target: CONFIG.targetElement || getMessage('videos.foundCountDocument') })); } } /** * Применяет настройки к видео элементу * @param {HTMLVideoElement} video - HTML элемент видео */ function applyVideoSettings(video) { // Сохраняем оригинальные атрибуты, чтобы восстановить важные const originalSrc = video.getAttribute('src'); const hadPoster = video.hasAttribute('poster'); const originalPoster = hadPoster ? video.getAttribute('poster') : null; // Очищаем атрибуты видео cleanVideoAttributes(video, hadPoster); // Устанавливаем свойства из конфига setVideoProperties(video); // Восстанавливаем важные оригинальные атрибуты restoreOriginalAttributes(video, originalSrc, hadPoster, originalPoster); // Применяем стили setupVideoStyles(video); // Добавляем обработчики событий addVideoEventListeners(video); } /** * Проверяет, является ли источник видео валидным * @param {string} src - URL источника видео * @returns {boolean} - true если источник валидный */ function isValidVideoSource(src) { // http/https URL, data-URI, blob-URL, Относительный путь return src && (src.startsWith('http') || src.startsWith('data:') || src.startsWith('blob:') || src.startsWith('/')); } /** * Очищает атрибуты видео * @param {HTMLVideoElement} video - HTML элемент видео * @param {boolean} hadPoster - Был ли атрибут poster до очистки */ function cleanVideoAttributes(video, hadPoster) { // Перебираем все атрибуты видео Array.from(video.attributes).forEach(attr => { // Не удаляем src и poster (если он был) if (attr.name !== 'src' && !(attr.name === 'poster' && hadPoster)) { video.removeAttribute(attr.name); } }); } /** * Устанавливает свойства видео из конфига * @param {HTMLVideoElement} video - HTML элемент видео */ function setVideoProperties(video) { // Копируем все свойства из CONFIG.videoSettings в элемент видео Object.assign(video, CONFIG.videoSettings); // Если включена фиксация скорости воспроизведения if (CONFIG.videoSettings.fixPlaybackRate) { video.playbackRate = CONFIG.videoSettings.playbackRate; if (CONFIG.debug.logVideosFound) { console.log(getMessage('videos.playbackRateSet', { rate: CONFIG.videoSettings.playbackRate })); } } } /** * Восстанавливает оригинальные атрибуты * @param {HTMLVideoElement} video - HTML элемент видео * @param {string} originalSrc - Оригинальный src * @param {boolean} hadPoster - Был ли poster * @param {string|null} originalPoster - Оригинальный poster */ function restoreOriginalAttributes(video, originalSrc, hadPoster, originalPoster) { // Восстанавливаем src, если он был и валидный if (originalSrc && isValidVideoSource(originalSrc)) { try { video.src = originalSrc; } catch (e) { console.warn(getMessage('errors.srcError'), e); video.removeAttribute('src'); } } else { video.removeAttribute('src'); } // Восстанавливаем poster, если он был if (hadPoster && originalPoster) { video.poster = originalPoster; } } /** * Применяет стили к видео * @param {HTMLVideoElement} video - HTML элемент видео */ function setupVideoStyles(video) { // Копируем стили из CONFIG.videoStyles в style элемента видео Object.assign(video.style, CONFIG.videoStyles); } /** * Добавляет обработчики событий для видео * @param {HTMLVideoElement} video - HTML элемент видео */ function addVideoEventListeners(video) { // Блокировка контекстного меню (правой кнопки мыши) video.addEventListener('contextmenu', e => { e.preventDefault(); console.log(getMessage('blockers.contextMenu')); return false; }); // Управление воспроизведением по клику (play/pause) video.addEventListener('click', function () { if (this.paused) { this.play().catch(e => { console.warn(getMessage('errors.autoplayBlocked'), e); }); } else { this.pause(); } }); // Фиксация скорости воспроизведения (если включено) if (CONFIG.videoSettings.fixPlaybackRate) { video.addEventListener('ratechange', function () { // Если пользователь изменил скорость - сбрасываем на заданную if (this.playbackRate !== CONFIG.videoSettings.playbackRate) { console.log(getMessage('videos.playbackRateReset', { rate: CONFIG.videoSettings.playbackRate })); this.playbackRate = CONFIG.videoSettings.playbackRate; } }); } // Обработка ошибок видео video.addEventListener('error', function () { if (this.error) { console.error(getMessage('errors.videoError'), { code: this.error.code, message: this.error.message, src: this.currentSrc || this.src, networkState: this.networkState, readyState: this.readyState }); } else { console.error(getMessage('errors.videoErrorNoDetails')); } }); } /** * Настраивает MutationObserver для отслеживания новых видео * MutationObserver наблюдает за изменениями DOM и позволяет обрабатывать динамически добавляемые видео элементы */ function setupMutationObserver() { // Создаем наблюдатель с callback-функцией const observer = new MutationObserver(mutations => { if (CONFIG.debug.logMutations) { console.log(getMessage('mutationObserverCreate'), mutations); } // Перебираем все изменения mutations.forEach(mutation => { // Перебираем все добавленные узлы mutation.addedNodes.forEach(node => { // Если добавлен элемент video if (node.nodeName === 'VIDEO') { // Проверяем, находится ли он в целевом элементе (если targetElement указан) if (!CONFIG.targetElement || node.closest(CONFIG.targetElement)) { console.log(getMessage('videos.newVideoDetected')); applyVideoSettings(node); } } // Если добавлен контейнер, в котором могут быть видео else if (node.nodeType === Node.ELEMENT_NODE) { // Ищем все видео внутри добавленного элемента const videos = node.querySelectorAll('video'); videos.forEach(video => { // Проверяем, находится ли видео в целевом элементе if (!CONFIG.targetElement || video.closest(CONFIG.targetElement)) { console.log(getMessage('videos.nestedVideoDetected')); applyVideoSettings(video); } }); } }); }); }); // Выбираем элементы за которыми будем наблюдать // Если targetElement указан - наблюдаем за ним, иначе за body const observeTargets = CONFIG.targetElement ? document.querySelectorAll(CONFIG.targetElement) : [document.body]; // Если не нашли элементов для наблюдения - предупреждение if (observeTargets.length === 0) { console.warn(getMessage('videos.mutationObserverWarning', { target: CONFIG.targetElement })); return; } // Начинаем наблюдение за каждым целевым элементом observeTargets.forEach(target => { observer.observe(target, { // Наблюдать за добавлением/удалением дочерних элементов childList: true, // Наблюдать за всеми потомками subtree: true }); console.log(getMessage('mutationObserverStart'), target); }); } /** * Блокирует горячие клавиши для видео * Предотвращает использование стандартных комбинаций клавиш для управления видео (например, полноэкранный режим) */ function setupHotkeyBlocker() { document.addEventListener('keydown', function (e) { // Получаем текущий активный элемент (который имеет фокус) const activeElement = document.activeElement; // Если активный элемент - видео if (activeElement?.nodeName === 'VIDEO') { // Проверяем, находится ли видео в целевом элементе (если targetElement указан) const isTargetVideo = !CONFIG.targetElement || activeElement.closest(CONFIG.targetElement); if (isTargetVideo) { // Блокировка Ctrl+клавиши (s, c, p, f) if (e.ctrlKey && ['s', 'c', 'p', 'f'].includes(e.key.toLowerCase())) { if (CONFIG.debug.logHotkeys) { console.log(getMessage('blockers.ctrlKey', { key: e.key })); } // Отменяем стандартное действие e.preventDefault(); // Останавливаем всплытие события e.stopPropagation(); } // Блокировка F12 (открытие инструментов разработчика) if (e.key === 'F12') { if (CONFIG.debug.logHotkeys) { console.log(getMessage('blockers.f12Key')); } e.preventDefault(); } } } }); } // Запуск скрипта после загрузки кода initVideoController();