Live Chat on YouTube Mobile (Auto Load + Auto Recreate + Viewer Count)

Auto-loads YouTube Live Chat on mobile with viewer count.

// ==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();
})();