|
Местный житель
Доп. информация
|
Регистрация: 11.06.2014
Сообщений: 66
Поблагодарил(а): 15
Поблагодарили: 13 / 10
|
|
Новогодний подарок всем соискателям - расширенное управление embed плеерами (YT и PT)
Давным давно задался вопросом оптимизировать загрузку тяжелого содержимого форума. За недавнее время написал механизм, который постепенно дорабатывал.
Хочу, в качестве новогоднего подгона, для всех соискателей выложить результат нескольких бессонных ночей и потраченных выходных...
1. YouTube:
Шаблон FOOTER:
Код:
<style>
.youtube-lazy {
position: relative;
cursor: pointer;
background: #000;
overflow: hidden;
display: inline-block;
}
.youtube-lazy img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
transition: opacity 0.3s ease;
}
.youtube-lazy .play-button {
position: absolute;
top: 50%;
left: 50%;
width: 80px;
height: 80px;
background: url('/multimedia/yt_plbtn.png') no-repeat center/contain;
transform: translate(-50%, -50%);
pointer-events: none;
z-index: 20;
}
.youtube-overlay {
position: absolute;
bottom: 12px;
left: 12px;
right: 12px;
background: rgba(0,0,0,0.7);
color: white;
padding: 12px 16px;
border-radius: 8px;
z-index: 10;
opacity: 0;
transition: opacity 0.4s ease;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 12px;
}
.youtube-overlay.show {
opacity: 1;
}
.youtube-icon {
width: 43px !important;
height: 43px !important;
flex-shrink: 0;
background: url('/multimedia/pic/YT.png') center/contain no-repeat !important;
background-color: transparent !important;
}
.youtube-title {
font-weight: 600;
font-size: clamp(14px, 4vw, 20px);
line-height: 1.3;
flex: 1;
min-width: 0;
}
.youtube-author {
font-size: 13px;
opacity: 0.85;
font-weight: 500;
}
.youtube-author a {
color: white !important;
text-decoration: none !important;
}
.youtube-separator {
width: 2px;
height: 32px;
flex-shrink: 0;
background: repeating-linear-gradient(
to bottom,
#fff 0px,
#fff 2px,
transparent 2px,
transparent 6px
);
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
function smartLazyLoad(players) {
const visibleVideos = [];
const hiddenVideos = [];
players.forEach(player => {
const rect = player.getBoundingClientRect();
if (rect.top < window.innerHeight && rect.bottom > 0) {
visibleVideos.push(player);
} else {
hiddenVideos.push(player);
}
});
visibleVideos.forEach(player => {
player.classList.add('lazy-processed');
const img = player.querySelector('img');
if (img && !player.isUnavailable) {
img.src = 'https://img.youtube.com/vi/' + player.videoId + '/maxresdefault.jpg';
}
});
setTimeout(() => {
hiddenVideos.forEach(player => {
player.classList.add('lazy-processed');
const img = player.querySelector('img');
if (img && !player.isUnavailable) {
img.src = 'https://img.youtube.com/vi/' + player.videoId + '/maxresdefault.jpg';
}
});
}, 1000);
}
function getYouTubeID(url) {
var regExp = /(?:youtube\.com.*(?:[?&]v=)|youtu\.be\/)([^&#]+)/;
var match = url.match(regExp);
return (match && match[1]) ? match[1] : null;
}
var players = document.querySelectorAll('.youtube-lazy');
players.forEach(function(player) {
var url = player.getAttribute('data-url') || '';
var width = 640;
var height = 360;
player.style.width = width + 'px';
player.style.height = height + 'px';
var videoId = getYouTubeID(url);
if (!videoId) return;
player.isUnavailable = false;
player.imgRetries = 0;
player.videoId = videoId;
player.thumbs = ['maxresdefault.jpg', 'hqdefault.jpg', 'sddefault.jpg', 'mqdefault.jpg', 'default.jpg'];
var img = player.querySelector('img');
if (img) {
img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==';
img.style.opacity = '0';
img.onload = function() {
if (player.isUnavailable) return;
if (this.naturalWidth > 120 || this.naturalHeight > 90) {
this.style.opacity = '1';
} else if (player.imgRetries < 4) {
player.imgRetries++;
this.src = 'https://img.youtube.com/vi/' + videoId + '/' + player.thumbs[player.imgRetries];
}
};
img.onerror = function() {
if (player.isUnavailable) return;
if (player.imgRetries < 4) {
player.imgRetries++;
this.src = 'https://img.youtube.com/vi/' + videoId + '/' + player.thumbs[player.imgRetries];
}
};
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting && !player.isUnavailable && !player.classList.contains('lazy-processed')) {
img.src = 'https://img.youtube.com/vi/' + player.videoId + '/maxresdefault.jpg';
observer.unobserve(img);
}
});
});
observer.observe(img);
}
}
var playButton = document.createElement('div');
playButton.className = 'play-button';
player.appendChild(playButton);
function clickHandler() {
var iframe = document.createElement('iframe');
iframe.setAttribute('src', 'https://www.youtube.com/embed/' + videoId + '?autoplay=1&rel=0&modestbranding=1&playsinline=1&origin=https://[ВАШ_ДОМЕН]');
iframe.setAttribute('frameborder', '0');
iframe.setAttribute('allowfullscreen', '1');
iframe.setAttribute('referrerpolicy', 'strict-origin-when-cross-origin');
iframe.style.width = '100%';
iframe.style.height = '100%';
this.innerHTML = '';
this.appendChild(iframe);
}
player.addEventListener('click', clickHandler);
player._clickHandler = clickHandler;
function showYouTubeLink() {
var overlay = document.createElement('div');
overlay.className = 'youtube-overlay';
overlay.innerHTML = `
<div class="youtube-icon"></div>
<div class="youtube-title">Просмотр только на YouTube</div>
<div class="youtube-separator"></div>
<div class="youtube-author">
<a href="ХТТПС://ВВВ.ЮТУБ.КОМ/watch?v=${videoId}" target="_blank">
Перейти на YouTube
</a>
</div>
`;
player.appendChild(overlay);
setTimeout(() => overlay.classList.add('show'), 400);
}
function showUnavailable() {
player.isUnavailable = true;
var img = player.querySelector('img');
if (img) {
img.style.display = 'none';
}
var ytImg = document.createElement('img');
ytImg.src = `https://i.ytimg.com/vi/${videoId}/mqdefault.jpg`;
ytImg.style.cssText = 'width:100%;height:100%;object-fit:cover;display:block;';
ytImg.onerror = function() {
this.style.background = '#000';
};
player.appendChild(ytImg);
var overlay = document.createElement('div');
overlay.className = 'youtube-overlay';
overlay.innerHTML = `
<div class="youtube-icon"></div>
<div class="youtube-title">ВИДЕО НЕДОСТУПНО</div>
<div class="youtube-separator"></div>
<div class="youtube-author">Видео удалено или недоступно</div>
`;
player.appendChild(overlay);
setTimeout(() => overlay.classList.add('show'), 400);
player.style.cursor = 'default';
player.removeEventListener('click', player._clickHandler);
}
function loadMetadata(retries = 0) {
fetch('https://www.youtube.com/oembed?url=ХТТПС://ВВВ.ЮТУБ.КОМ/watch?v=' + videoId + '&format=json')
.then(response => {
if (!response.ok) throw new Error('no');
return response.json();
})
.then(oembed => {
if (oembed.title && oembed.author_name) {
var overlay = document.createElement('div');
overlay.className = 'youtube-overlay';
overlay.innerHTML = `
<div class="youtube-icon" alt="YouTube"></div>
<div class="youtube-title">${oembed.title}</div>
<div class="youtube-separator"></div>
<div class="youtube-author">${oembed.author_name}</div>
`;
player.appendChild(overlay);
setTimeout(() => overlay.classList.add('show'), 400);
} else retryOrFallback();
})
.catch(() => retryOrFallback());
function retryOrFallback() {
if (retries < 3) {
setTimeout(() => loadMetadata(retries + 1), (retries + 1) * 400);
} else {
const checkImage = () => {
var img = player.querySelector('img');
if (img && img.complete && img.naturalWidth > 120 && img.naturalHeight > 90) {
showYouTubeLink();
} else if (img && img.complete) {
showUnavailable();
} else {
setTimeout(checkImage, 500);
}
};
setTimeout(checkImage, 1000);
}
}
}
loadMetadata(0);
});
setTimeout(() => {
smartLazyLoad(Array.from(document.querySelectorAll('.youtube-lazy')));
}, 100);
});
</script>
2. Также есть такой, стремительно набирающий популярность феномен как PeerTube:
Шаблон FOOTER:
Код:
<style>
.peertube-wrapper {
position: relative;
width: 100%;
max-width: 640px;
padding-bottom: 56.25%;
height: 0;
background-color: black;
display: inline-block !important;
vertical-align: top;
color: white;
font-weight: bold;
overflow: hidden;
box-sizing: border-box;
float: none !important;
clear: both;
line-height: 0;
}
.peertube-wrapper iframe.peertube-player {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: none;
display: block;
}
.peertube-wrapper .error-message {
text-align: center;
color: #ff4444;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 8px;
width: 100%;
height: 100%;
}
.peertube-wrapper .error-message img {
max-width: 80px;
height: auto;
}
.peertube-lazy {
display: inline-block !important;
vertical-align: top !important;
max-width: 640px;
line-height: 0 !important;
}
table .peertube-lazy, .postcontent .peertube-lazy, .postbody .peertube-lazy {
display: inline-block !important;
vertical-align: top !important;
}
</style>
<script>
const videoIdCache = new Map();
const embedUrlCache = new Map();
let lastMutationCheck = 0;
const THROTTLE_DELAY = 500;
const isAndroid = /Android/i.test(navigator.userAgent);
let androidVideosLoaded = 0;
let androidScrollReset = 0;
function initPeertubeContainer(container) {
if (container.dataset.processed) return;
container.dataset.processed = 'true';
if (isAndroid) {
if (container.dataset.blocked) return;
if (androidVideosLoaded >= 4) {
container.dataset.blocked = 'true';
container.innerHTML = '';
container.style.position = 'relative';
container.style.background = '#000';
container.innerHTML = `<div style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: #000; display: flex; flex-direction: column; align-items: center; justify-content: center; color: #ff6b6b; font-size: 14px; padding: 20px; text-align: center;">
<img src="https://[ВАШ ДОМЕН]/client/assets/images/mascot/defeated.svg" style="width: 50px; height: auto; margin-bottom: 10px;">
<div style="font-weight: bold;">Много видео</div>
<small style="color: #999; font-size: 12px;">Первые 4 загружены</small>
</div>`;
return;
}
}
const loadIframe = () => {
const fullUrl = container.getAttribute('data-url');
const errorImageUrl = 'https://[ВАШ ДОМЕН]/client/assets/images/mascot/defeated.svg';
if (isAndroid) {
if (container._debounceTimeout) clearTimeout(container._debounceTimeout);
container._debounceTimeout = setTimeout(() => {
if (container.contains(document.querySelector('[id^="loading-"]'))) return;
container.innerHTML = `<div style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: #000; display: flex; align-items: center; justify-content: center; color: #666; font-size: 14px; z-index: 10;" id="loading-${Date.now()}">Загрузка...</div>`;
container.style.position = 'relative';
container.style.background = '#000';
loadIframeContent(fullUrl, errorImageUrl, container);
}, 100);
} else {
container.innerHTML = `<div style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: #000; display: flex; align-items: center; justify-content: center; color: #666; font-size: 14px; z-index: 10;" id="loading-${Date.now()}">Загрузка...</div>`;
container.style.position = 'relative';
container.style.background = '#000';
loadIframeContent(fullUrl, errorImageUrl, container);
}
};
const loadIframeContent = (fullUrl, errorImageUrl, container) => {
setTimeout(() => {
try {
const url = new URL(fullUrl);
let videoId = videoIdCache.get(fullUrl);
if (!videoId) {
const pathParts = url.pathname.split('/');
videoId = pathParts[pathParts.length - 1];
while (videoId === '' || videoId === 'watch' || videoId === 'videos') {
pathParts.pop();
videoId = pathParts[pathParts.length - 1];
}
videoIdCache.set(fullUrl, videoId);
}
if (!videoId) throw new Error('Не найден videoId');
let embedUrl = embedUrlCache.get(videoId);
if (!embedUrl) {
embedUrl = `${url.origin}/videos/embed/${videoId}?autoplay=0&muted=0&loop=0&controlBar=1&fullScreenButton=1&cc=0&title=1&warningTitle=0`;
embedUrlCache.set(videoId, embedUrl);
}
const iframe = document.createElement('iframe');
iframe.src = embedUrl;
iframe.width = '100%';
iframe.height = '100%';
iframe.frameBorder = 0;
iframe.allowFullscreen = true;
iframe.loading = 'lazy';
iframe.allow = 'autoplay; fullscreen';
iframe.style.position = 'absolute';
iframe.style.top = '0';
iframe.style.left = '0';
iframe.style.zIndex = '5';
iframe.style.background = 'black';
iframe.style.opacity = '0';
let iframeLoaded = false;
iframe.onload = () => {
iframeLoaded = true;
if (isAndroid) androidVideosLoaded++;
const loadingEl = container.querySelector('[id^="loading-"]');
if (loadingEl) loadingEl.remove();
iframe.style.opacity = '1';
clearTimeout(container._debounceTimeout);
};
iframe.onerror = () => {};
container.appendChild(iframe);
setTimeout(() => {
if (!iframeLoaded) {
const loadingEl = container.querySelector('[id^="loading-"]');
if (loadingEl) loadingEl.remove();
if (!container.querySelector('iframe[src]')) {
container.innerHTML = `<div style="text-align:center; padding: 20px;">
<img src="${errorImageUrl}" alt="Ошибка" style="max-width:100%; height:auto;">
<div style="color: red; font-weight: bold; margin-top: 8px;">Видео недоступно</div>
</div>`;
}
}
}, isAndroid ? 7000 : 5000);
} catch (e) {
const loadingEl = container.querySelector('[id^="loading-"]');
if (loadingEl) loadingEl.remove();
container.innerHTML = `<div style="text-align:center; padding: 20px;">
<img src="${errorImageUrl}" alt="Ошибка" style="max-width:100%; height:auto;">
<div style="color: red; font-weight: bold; margin-top: 8px;">${e.message}</div>
</div>`;
}
}, 10);
};
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting && !entry.target.dataset.loadingStarted) {
entry.target.dataset.loadingStarted = 'true';
loadIframe();
observer.unobserve(container);
}
});
}, {
threshold: 0,
rootMargin: isAndroid ? '200px 0px 400px 0px' : `${window.innerHeight * 8}px 0px ${window.innerHeight * 8}px 0px`
});
observer.observe(container);
} else {
loadIframe();
}
}
let scrollObserver = null;
function setupScrollObserver() {
if (scrollObserver || !('IntersectionObserver' in window)) return;
scrollObserver = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const container = entry.target;
if (!container.dataset.processed) {
initPeertubeContainer(container);
}
}
});
}, {
threshold: 0,
rootMargin: `${window.innerHeight * 10}px 0px ${window.innerHeight * 10}px 0px`
});
requestAnimationFrame(() => {
document.querySelectorAll('.peertube-lazy').forEach(container => {
scrollObserver.observe(container);
});
});
}
const mutationObserver = new MutationObserver((mutations) => {
const now = Date.now();
if (now - lastMutationCheck < THROTTLE_DELAY) return;
lastMutationCheck = now;
requestAnimationFrame(() => {
let hasNewVideos = false;
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1) {
const newContainers = node.querySelectorAll('.peertube-lazy');
newContainers.forEach(container => {
initPeertubeContainer(container);
if (scrollObserver) scrollObserver.observe(container);
});
if (newContainers.length > 0) hasNewVideos = true;
}
});
});
if (hasNewVideos) {
setupScrollObserver();
}
});
});
mutationObserver.observe(document.body, { childList: true, subtree: true });
setTimeout(() => {
setupScrollObserver();
}, 800);
let ticking = false;
window.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(() => {
setupScrollObserver();
if (isAndroid && Math.abs(window.scrollY - androidScrollReset) > 500) {
androidVideosLoaded = 0;
androidScrollReset = window.scrollY;
}
ticking = false;
});
ticking = true;
}
}, { passive: true });
setTimeout(() => {
const allVideos = document.querySelectorAll('.peertube-lazy:not([data-processed])');
if (isAndroid) {
androidVideosLoaded = 0;
allVideos.forEach((container, index) => {
setTimeout(() => {
if (!container.dataset.processed && !container.dataset.blocked) {
initPeertubeContainer(container);
}
}, index * 200);
});
} else {
allVideos.forEach((container, index) => {
const delay = (index % 5) * 50;
setTimeout(() => {
initPeertubeContainer(container);
}, delay);
});
}
}, 1500);
</script>
Как общий результат: очень быстрая загрузка содержимого страниц вашего форума vBulletin 4.x.x! Даже если на странице запощено очень много видео. Бонусом отдельно идет оптимизация под мобильные платформы, т.к. механизм у них чуточку другой (с этим я дополнительно помучился).
Не забудьте подправить пути на ресурсы в скриптах и нарисовать свои иконки для YT-варианта. (Также исправьте "ХТТПС://ВВВ.ЮТУБ.КОМ" на нормальный адрес, а то почему-то здешний BB код "CODE" не экранирует всё как надо.)
С наступающим всех!!! 
|