// ==UserScript==
// @name Live Chat on YouTube Mobile (Auto Load + Auto Recreate + Viewer Count)
// @version 1.3
// @description Auto-loads YouTube Live Chat on mobile with viewer count.
// @match https://m.youtube.com/*
// @grant none
// @license MIT
// @namespace https://greasyfork.org/users/1360319
// ==/UserScript==
(function() {
'use strict';
// --- Chat Container ---
const chatContainer = document.createElement('div');
Object.assign(chatContainer.style, {
display: 'none',
position: 'fixed',
width: '100%',
top: '250px',
bottom: '0',
left: '0',
right: '0',
borderTop: '1px solid #555',
backgroundColor: '#333',
zIndex: '9998',
overflow: 'hidden'
});
// --- Loader Spinner ---
const loader = document.createElement('div');
Object.assign(loader.style, {
border: '4px solid #555',
borderTop: '4px solid #ff0000',
borderRadius: '50%',
width: '30px',
height: '30px',
animation: 'spin 1s linear infinite',
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
zIndex: '10000',
display: 'none'
});
const styleSheet = document.createElement("style");
styleSheet.textContent = `
@keyframes spin {
0% { transform: translate(-50%, -50%) rotate(0deg); }
100% { transform: translate(-50%, -50%) rotate(360deg); }
}
.YtmBottomSheetOverlayRendererOverlayContainer{opacity:0!important}
bottom-sheet-container{
z-index:55000!important;
}
#ytm-chat-top-toggle {
position: absolute;
top: 10px;
left: 50%;
transform: translateX(-50%);
background: transparent;
color: grey;
border: none;
border-radius: 6px;
padding: 6px 12px;
font-size: 14px;
cursor: pointer;
z-index: 10001;
touch-action: manipulation;
}
`;
document.head.appendChild(styleSheet);
// --- Add top-center toggle button ---
const topToggleBtn = document.createElement('button');
topToggleBtn.id = 'ytm-chat-top-toggle';
topToggleBtn.textContent = 'Hide Chat';
chatContainer.appendChild(topToggleBtn);
chatContainer.appendChild(loader);
document.body.appendChild(chatContainer);
// --- State ---
let currentVideoId,lastViewerText,lastVideoId,chatIframe,viewerInterval,buttonCheckInterval,streamTimeSpan,actualStartTime,elapsedMs,streamTimeInterval,ti= null;
let ii=false;
const showLoader = () => {
loader.style.display = 'block';
chatContainer.style.backgroundColor = '#111';
};
const hideLoader = () => {
loader.style.display = 'none';
chatContainer.style.backgroundColor = '#333';
};
const createStreamTimeSpan = (elapsedMs) => {
if (isNaN(elapsedMs) || document.querySelector('#streamTimeSpan')) return;
const controlsContainer = document.querySelector('.player-controls-top')?.parentElement;
if (controlsContainer) {
streamTimeSpan=null;
streamTimeSpan = document.createElement('span');
streamTimeSpan.id='streamTimeSpan';
Object.assign(streamTimeSpan.style, {
position: 'absolute',
bottom: '20%',
left: '50%',
transform: 'translateX(-50%)',
backgroundColor: 'rgba(0,0,0,0.5)',
color: '#fff',
padding: '4px 8px',
borderRadius: '4px',
fontSize: '14px',
zIndex: '9999',
pointerEvents: 'none'
});
controlsContainer.appendChild(streamTimeSpan);
const totalSeconds = Math.floor(elapsedMs / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
streamTimeSpan.textContent = `${hours.toString().padStart(2,'0')}:${minutes.toString().padStart(2,'0')}:${seconds.toString().padStart(2,'0')}`;
streamTimeInterval = setInterval(() => {
// in milliseconds
elapsedMs+=1000;
const totalSeconds = Math.floor(elapsedMs / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
streamTimeSpan.textContent = `${hours.toString().padStart(2,'0')}:${minutes.toString().padStart(2,'0')}:${seconds.toString().padStart(2,'0')}`;
}, 1000);
}
};
// --- Helper to format numbers with commas ---
const formatNumber = (num) => {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
};
// --- Helper to fetch "currently watching" text ---
const getViewerText = async (force = false) => {
const videoIdMatch = window.location.search.match(/v=([^&]+)/);
const videoId = videoIdMatch?.[1];
if (!videoId) return { text: null, startTime: null };
// Skip if already fetched for this video (unless forced)
if (!force && lastVideoId === videoId && lastViewerText) {
return { text: lastViewerText, elapsed: elapsedMs };
}
const apiUrl = `https://www.googleapis.com/youtube/v3/videos?part=liveStreamingDetails&id=${videoId}&key=AIzaSyCXvukhVfjDbvL_mhmLWbMEwkv0ROHeegM`;
try {
const response = await fetch(apiUrl);
const data = await response.json();
const liveDetails = data.items?.[0]?.liveStreamingDetails || {};
actualStartTime = liveDetails.actualStartTime || document.querySelector('meta[itemprop="startDate"]')?.content || null;
const start = new Date(actualStartTime);
const now = new Date();
elapsedMs = now - start;
const viewers = liveDetails.concurrentViewers;
if (viewers) {
const formatted = formatNumber(viewers);
lastViewerText = `${formatted} watching now`;
} else {
lastViewerText = document.querySelector('.secondary-text > .yt-core-attributed-string')?.textContent?.trim() || '';
}
lastVideoId = videoId;
return { text: lastViewerText, elapsed: elapsedMs };
} catch (error) {
console.error('Error fetching viewer count:', error);
const metaStart = document.querySelector('meta[itemprop="startDate"]')?.content || null;
const start = new Date(metaStart);
const now = new Date();
elapsedMs = now - start;
return { text: document.querySelector('.secondary-text > .yt-core-attributed-string')?.textContent?.trim() || '', elapsed: elapsedMs };
}
};
// --- Create and insert "Currently Watching" and "Hide Chat" buttons ---
const ensureChatButtons = () => {
const controlsTop = document.querySelector('.player-controls-top');
if (controlsTop) {
let watchBtn = document.getElementById('ytm-currently-watching-btn');
let chatButton = document.getElementById('ytm-hide-chat-btn');
// Create Currently Watching button
if (!watchBtn) {
watchBtn = document.createElement('button');
watchBtn.id = 'ytm-currently-watching-btn';
watchBtn.style.zIndex = '5555';
Object.assign(watchBtn.style, {
background: 'linear-gradient(to bottom, rgba(0,0,0,0.6), rgba(0,0,0,0))',
color: '#fff',
border: 'none',
borderRadius: '4px',
padding: '4px 6px',
marginRight: '6px',
cursor: 'default',
fontSize: '14px'
});
// Use cached text/startTime if already fetched
if (lastViewerText) {
watchBtn.textContent = lastViewerText;
} else {
const waitForViewerText = () => {
if (lastViewerText) {
watchBtn.textContent = lastViewerText;
} else {
setTimeout(waitForViewerText, 300);
}
};
waitForViewerText();
}
}
if (elapsedMs && !document.querySelector('#streamTimeSpan')) {
createStreamTimeSpan(elapsedMs);
}
// Create Hide/Display Chat button
if (!chatButton) {
chatButton = document.createElement('button');
chatButton.id = 'ytm-hide-chat-btn';
chatButton.textContent = 'Hide Chat';
chatButton.style.zIndex = '5555';
Object.assign(chatButton.style, {
background: 'linear-gradient(to bottom, rgba(0,0,0,0.4), rgba(0,0,0,0))',
color: '#fff',
border: 'none',
borderRadius: '4px',
padding: '4px 6px',
marginRight: '6px',
touchAction: 'manipulation',
cursor: 'pointer',
fontSize: '14px'
});
}
if (!document.getElementById('ytm-hide-chat-btn')) {
controlsTop.insertBefore(chatButton, controlsTop.firstChild);
chatButton.addEventListener('click', toggleChatVisibility, { passive: true });
chatButton.addEventListener('touchstart', toggleChatVisibility, { passive: true });
topToggleBtn.addEventListener('click', toggleChatVisibility, { passive: true });
topToggleBtn.addEventListener('touchstart', toggleChatVisibility, { passive: true });
}
if (!document.getElementById('ytm-currently-watching-btn')) {
controlsTop.insertBefore(watchBtn, chatButton);
}
}
};
// --- Unified toggle function ---
const toggleChatVisibility = () => {
if(!ii){
ii=true;
const below = document.querySelector('.watch-below-the-player');
const chatButton = document.getElementById('ytm-hide-chat-btn');
const hidden = chatContainer.style.visibility === 'hidden';
if (hidden) {
chatContainer.style.visibility = 'visible';
if (below) below.style.display = 'none';
topToggleBtn.textContent = 'Hide Chat';
if (chatButton) chatButton.textContent = 'Hide Chat';
} else {
//avoid accidental touches/clicks
const blocker = document.createElement('div');
blocker.style.position = 'fixed';
blocker.style.top = 0;
blocker.style.left = 0;
blocker.style.width = '100%';
blocker.style.height = '100%';
blocker.style.background = 'transparent';
blocker.style.zIndex = 999999;
blocker.style.pointerEvents = 'auto';
document.body.appendChild(blocker);
setTimeout(() => blocker.remove(), 200);
if (below){
below.style.display = '';
}
chatContainer.style.visibility = 'hidden';
if (chatButton) chatButton.textContent = 'Show Chat';
}
clearTimeout(ti);
ti= setTimeout(() => ii=false, 200);
}
};
// --- Create chat iframe ---
const createChatIframe = async (videoId) => {
if (chatIframe && chatIframe.parentElement) chatIframe.remove();
const { text, elapsed } = await getViewerText(true);
lastViewerText = text;
elapsedMs = elapsed;
chatIframe = document.createElement('iframe');
Object.assign(chatIframe.style, {
width: '100%',
height: '100%',
border: 'none',
maxWidth: '100%',
opacity: '0'
});
chatIframe.src = `https://www.youtube.com/live_chat?v=${videoId}&embed_domain=${window.location.hostname}`;
chatIframe.onload = () => {
hideLoader();
chatIframe.style.opacity = '1';
};
chatContainer.appendChild(chatIframe);
currentVideoId = videoId;
// Clear old interval if any
[viewerInterval, buttonCheckInterval, streamTimeInterval].forEach(i => { if (i) clearInterval(i); });
viewerInterval = buttonCheckInterval = streamTimeInterval = null;
// Update viewer count periodically
viewerInterval = setInterval(async () => {
const watchBtn = document.getElementById('ytm-currently-watching-btn');
if (watchBtn) {
const { text: updatedText } = await getViewerText(true);
watchBtn.textContent = updatedText;
if(document.querySelector('.secondary-text > .yt-core-attributed-string'))
document.querySelector('.secondary-text > .yt-core-attributed-string').textContent=updatedText;
}
}, 120000);
ensureChatButtons();
if (!buttonCheckInterval) {
buttonCheckInterval = setInterval(ensureChatButtons, 1000);
}
};
// --- Close chat and cleanup ---
const closeChat = () => {
lastViewerText=null;
lastVideoId = null;
[viewerInterval, buttonCheckInterval, streamTimeInterval].forEach(i => clearInterval(i));
viewerInterval = buttonCheckInterval = streamTimeInterval = null;
if (chatContainer.style.display !== 'none') {
if (chatIframe && chatIframe.parentElement) chatIframe.remove();
chatIframe = null;
chatContainer.style.display = 'none';
hideLoader();
}
if (streamTimeSpan && streamTimeSpan.parentElement) {
streamTimeSpan.remove();
streamTimeSpan = null;
}
};
// --- Check if current page is a live video ---
const isVideoPage = () => {
return (
window.location.pathname === '/watch' &&
window.location.search.includes('v=') &&
(
document.querySelector('.ytwPlayerTimeDisplayLiveDot.ytwPlayerTimeDisplayPill > div > span > .yt-core-attributed-string')?.textContent.includes('Live') ||
document.querySelector('.secondary-text > .yt-core-attributed-string')?.textContent.includes('watching now')
)
);
};
// --- Auto-load chat ---
const autoLoadChat = () => {
if (isVideoPage()) {
const videoIdMatch = window.location.search.match(/v=([^&]+)/);
if (!videoIdMatch) return;
const videoId = videoIdMatch[1];
showLoader();
createChatIframe(videoId);
chatContainer.style.display = 'block';
chatContainer.style.visibility = 'visible';
const below = document.querySelector('.watch-below-the-player');
if (below) below.style.display = "none";
} else {
const below = document.querySelector('.watch-below-the-player');
if (below) below.style.display = "";
closeChat();
}
};
// --- Monitor video changes ---
setInterval(() => {
// remove "ready to shop" banner
document.querySelector('.YtmBottomSheetOverlayRendererHeader')?.children[1]?.children[0]?.click();
const videoIdMatch = window.location.search.match(/v=([^&]+)/);
const newVideoId = videoIdMatch ? videoIdMatch[1] : null;
if (isVideoPage()) {
if (chatContainer.style.display === 'none' || newVideoId !== currentVideoId) {
autoLoadChat();
}
const videoPlayer = document.querySelector('#player');
if (videoPlayer) {
const rect = videoPlayer.getBoundingClientRect();
chatContainer.style.top = `${rect.bottom}px`;
chatContainer.style.height = 'auto';
}
} else {
closeChat();
currentVideoId = null;
}
}, 1000);
autoLoadChat();
})();