Live Chat on YouTube Mobile

Integrates the live chat into the mobile version of YouTube

< 腳本Live Chat on YouTube Mobile的回應

評論:正評 - 腳本一切正常

§
發表於:2025-10-22

hi, it works great thank you but the only complaint is that the button appears on normal videos too and also there are some scrolling issues caused by the related videos in the background,i've fixed both of these issues by first only making the button appear when the 'live' span is present inside the player container, also when chat is shown i've set the related videos and shorts display to "none" so there are no scrolling issues and also removed the mutation observers in favor of a simple 2 seconds interval that detects url changes.
// ==UserScript==
// @name Live Chat on YouTube Mobile
// @version 1.2.1
// @description Integrates the live chat into the mobile version of YouTube
// @match https://m.youtube.com/*
// @grant none
// @namespace https://greasyfork.org/en/scripts/519614-live-chat-on-youtube-mobile
// @license MIT
// ==/UserScript==

(function() {
'use strict';

const button = document.createElement('button');
button.innerText = '💬';
button.style.position = 'fixed';
button.style.bottom = '64px';
button.style.right = '20px';
button.style.zIndex = '9999';
button.style.padding = '10px';
button.style.backgroundColor = '#ff0000';
button.style.color = '#fff';
button.style.border = 'none';
button.style.borderRadius = '50%';
button.style.width = '45px';
button.style.height = '45px';
button.style.fontSize = '20px';
button.style.lineHeight = '0';
button.style.boxShadow = '0 3px 6px rgba(0,0,0,0.3)';
button.style.cursor = 'pointer';
button.style.opacity = '0.7'; // Added opacity

document.body.appendChild(button);

const chatContainer = document.createElement('div');
chatContainer.style.display = 'none';
chatContainer.style.position = 'fixed';
chatContainer.style.width = '100%';
chatContainer.style.top = '250px';
chatContainer.style.bottom = '0';
chatContainer.style.height = 'auto';
chatContainer.style.left = '0';
chatContainer.style.right = '0';
chatContainer.style.borderTop = '1px solid #555';
chatContainer.style.backgroundColor = '#333';
chatContainer.style.zIndex = '9998';
chatContainer.style.overflow = 'hidden';

const loader = document.createElement('div');
loader.style.border = '4px solid #555';
loader.style.borderTop = '4px solid #ff0000';
loader.style.borderRadius = '50%';
loader.style.width = '30px';
loader.style.height = '30px';
loader.style.animation = 'spin 1s linear infinite';
loader.style.position = 'absolute';
loader.style.top = '50%';
loader.style.left = '50%';
loader.style.transform = 'translate(-50%, -50%)';
loader.style.zIndex = '10000';
loader.style.display = 'none';

const styleSheet = document.createElement("style");
styleSheet.type = "text/css";
styleSheet.innerText = "@keyframes spin { 0% { transform: translate(-50%, -50%) rotate(0deg); } 100% { transform: translate(-50%, -50%) rotate(360deg); } }";
document.head.appendChild(styleSheet);

chatContainer.appendChild(loader);

const chatIframe = document.createElement('iframe');
chatIframe.style.width = '100%';
chatIframe.style.height = '100%';
chatIframe.style.border = 'none';
chatIframe.style.maxWidth = '100%';
chatIframe.style.opacity = '0';

let isIframeLoaded = false;
let currentVideoId = null;

const showLoader = () => {
const darkerBackground = '#111';
loader.style.display = 'block';
chatIframe.style.opacity = '0';
chatContainer.style.backgroundColor = darkerBackground;
loader.style.backgroundColor = darkerBackground;
};

const hideLoader = () => {
loader.style.display = 'none';
chatIframe.style.opacity = '1';
chatContainer.style.backgroundColor = '#333';
};

const closeChat = () => {
if (chatContainer.style.display !== 'none') {
chatContainer.style.display = 'none';
button.innerText = '💬';
hideLoader();
}
};

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');
};

const applyIframeStyles = () => {
if (!isIframeLoaded) {
try {
const iframeDocument = chatIframe.contentWindow.document;
const style = iframeDocument.createElement('style');
style.textContent = `
.style-scope.yt-live-chat-text-message-renderer {
font-size: 12px !important;
}
.style-scope.yt-live-chat-text-message-renderer #author-name {
font-size: 12px !important;
}
`;
iframeDocument.head.appendChild(style);
isIframeLoaded = true;
} catch (e) {
// Silently fail if styles can't be applied
}
}
};

chatIframe.onload = function() {
applyIframeStyles();
hideLoader();
};

button.onclick = function() {
if (chatContainer.style.display === 'none') {
const videoIdMatch = window.location.search.match(/v=([^&]+)/);
if (videoIdMatch) {
const videoId = videoIdMatch[1];

if (!chatIframe.parentElement) {
chatContainer.appendChild(chatIframe);
document.body.appendChild(chatContainer);
}

if (videoId !== currentVideoId) {
const newChatSrc = `https://www.youtube.com/live_chat?v=${videoId}&embed_domain=${window.location.hostname}`;

showLoader();

chatIframe.src = newChatSrc;
currentVideoId = videoId;
isIframeLoaded = false;
} else if (chatIframe.src === '') {
const newChatSrc = `https://www.youtube.com/live_chat?v=${videoId}&embed_domain=${window.location.hostname}`;
showLoader();
chatIframe.src = newChatSrc;
}

const videoPlayer = document.querySelector('video');
if (videoPlayer) {
const videoRect = videoPlayer.getBoundingClientRect();

const isVertical = videoRect.height > videoRect.width && videoRect.height > 100;

if (isVertical) {
chatContainer.style.top = '50%';
chatContainer.style.height = '50%';
} else {
chatContainer.style.top = `${videoRect.bottom}px`;
chatContainer.style.height = 'auto';
}

chatContainer.style.bottom = '0';
} else {
chatContainer.style.top = '250px';
chatContainer.style.height = 'auto';
}

chatContainer.style.display = 'block';
if( document.querySelector('.watch-below-the-player'))
document.querySelector('.watch-below-the-player').style.display="none";

button.innerText = '✖️';

chatIframe.focus();

} else {
alert('Could not find the video ID.');
}
} else {
if( document.querySelector('.watch-below-the-player'))
document.querySelector('.watch-below-the-player').style.display="";

closeChat();
}
};

const updateButtonVisibility = () => {
button.style.display = isVideoPage() ? 'block' : 'none';
if (!isVideoPage()) {
closeChat();
currentVideoId = null;
}

if (chatContainer.style.display !== 'none') {
const videoPlayer = document.querySelector('video');
if (videoPlayer) {
const videoRect = videoPlayer.getBoundingClientRect();

const isVertical = videoRect.height > videoRect.width && videoRect.height > 100;
if (isVertical) {
chatContainer.style.top = '50%';
chatContainer.style.height = '50%';
} else {
chatContainer.style.top = `${videoRect.bottom}px`;
chatContainer.style.height = 'auto';
}
}
}
};

// Check every 500ms for button visibility and video changes
setInterval(updateButtonVisibility, 500);

// Check every 2 seconds if we're still on a video page
setInterval(() => {
if (!isVideoPage() && chatContainer.style.display !== 'none') {
closeChat();
}
}, 2000);

updateButtonVisibility();

})();

§
發表於:2025-10-22

Hi again, here's the updated version with autoload and viewercount on the player and many optimizations :


// ==UserScript==
// @name Live Chat on YouTube Mobile (Auto Load + Auto Recreate + Viewer Count)
// @version 1.4
// @description Auto-loads YouTube Live Chat on mobile with viewer count and hide button.
// @match https://m.youtube.com/*
// @grant none
// @license MIT
// ==/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); } }";
document.head.appendChild(styleSheet);

chatContainer.appendChild(loader);
document.body.appendChild(chatContainer);

// --- State ---
let currentVideoId = null;
let chatIframe = null;
let viewerInterval = null; // global interval for updating viewer count

const showLoader = () => {
loader.style.display = 'block';
chatContainer.style.backgroundColor = '#111';
};
const hideLoader = () => {
loader.style.display = 'none';
chatContainer.style.backgroundColor = '#333';
};

// --- Helper to fetch "currently watching" text ---
const getViewerText = () => {
return (
document.querySelector('.secondary-text > .yt-core-attributed-string')?.textContent || '').trim();
};

const createChatIframe = (videoId) => {
if (chatIframe && chatIframe.parentElement) chatIframe.remove();

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
if (viewerInterval) {
clearInterval(viewerInterval);
viewerInterval = null;
}

// === Create and insert "Currently Watching" and "Hide Chat" buttons ===
let watchBtn = document.getElementById('ytm-currently-watching-btn');
let chatButton = document.getElementById('ytm-hide-chat-btn');

if (!watchBtn || !chatButton) {
// Create Currently Watching button
if (!watchBtn) {
watchBtn = document.createElement('button');
watchBtn.id = 'ytm-currently-watching-btn';
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: '6px 10px',
marginRight: '8px',
cursor: 'default',
fontSize: '14px'
});
}

// Create Hide/Display Chat button
if (!chatButton) {
chatButton = document.createElement('button');
chatButton.id = 'ytm-hide-chat-btn';
chatButton.textContent = 'Hide Chat';
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: '6px 10px',
marginRight: '8px',
cursor: 'pointer',
fontSize: '14px'
});

chatButton.onclick = () => {
const below = document.querySelector('.watch-below-the-player');
if (chatContainer.style.visibility !== 'hidden') {
chatContainer.style.visibility = 'hidden';
if (below) below.style.display = '';
chatButton.textContent = 'Display Chat';
} else {
chatContainer.style.visibility = 'visible';
if (below) below.style.display = 'none';
chatButton.textContent = 'Hide Chat';
}
};
}

const controlsTop = document.querySelector('.player-controls-top');
const insertButtons = () => {
const controlsTop = document.querySelector('.player-controls-top');
if (controlsTop) {
if (!document.getElementById('ytm-hide-chat-btn')) {
controlsTop.insertBefore(chatButton, controlsTop.firstChild);
}
if (!document.getElementById('ytm-currently-watching-btn')) {
controlsTop.insertBefore(watchBtn, chatButton);
}
} else {
setTimeout(insertButtons, 500);
}
};
insertButtons();
}

// Update the "Currently Watching" text every minute
const updateViewerText = () => {
const text = getViewerText();
watchBtn.textContent = text ? text : 'Live';
};

updateViewerText();
viewerInterval = setInterval(updateViewerText, 60000);
};

const closeChat = () => {
if (viewerInterval) {
clearInterval(viewerInterval);
viewerInterval = null;
}

if (chatContainer.style.display !== 'none') {
if (chatIframe && chatIframe.parentElement) chatIframe.remove();
chatIframe = null;
chatContainer.style.display = 'none';
hideLoader();
}
};

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')
)
);
};

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(() => {
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('video');
if (videoPlayer) {
const rect = videoPlayer.getBoundingClientRect();
const isVertical = rect.height > rect.width && rect.height > 100;
chatContainer.style.top = isVertical ? '50%' : `${rect.bottom}px`;
chatContainer.style.height = isVertical ? '50%' : 'auto';
}
} else {
closeChat();
currentVideoId = null;
}
}, 1000);

autoLoadChat();
})();

發表回覆

登入以回覆