Greasy Fork is available in English.
将 Meta 平台的视频默认音量设置为 10%,并在视频上增加一个方便调整音量的滑块界面。
当前为
// ==UserScript==
// @name Meta 社交媒體音量控制大師 (FB, IG, Threads)
// @name:zh-TW Meta 社交媒體音量控制大師 (FB, IG, Threads)
// @name:zh-CN Meta 社交媒体音量控制大师 (FB, IG, Threads)
// @name:en Meta Media Volume Master (FB, IG, Threads)
// @name:ja Meta メディア音量マスター (FB, IG, Threads)
// @namespace http://tampermonkey.net/
// @version 2.8
// @description 將 Meta 平台的影片預設音量設定為 10%,並在影片上增加一個方便調整音量的滑桿介面。
// @description:zh-TW 將 Meta 平台的影片預設音量設定為 10%,並在影片上增加一個方便調整音量的滑桿介面。
// @description:zh-CN 将 Meta 平台的视频默认音量设置为 10%,并在视频上增加一个方便调整音量的滑块界面。
// @description:en Sets the default volume of videos on Meta platforms (FB, IG, Threads) to 10% and adds a convenient volume slider overlay.
// @description:ja Metaプラットフォーム(FB、IG、Threads)の動画のデフォルト音量を10%に設定し、音量を調整しやすいスライダーオーバーレイを追加します。
// @author You
// @match *://*.facebook.com/*
// @match *://*.instagram.com/*
// @match *://*.threads.net/*
// @match *://*.threads.com/*
// @grant none
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
// ==========================================================
// 設定區域
// ==========================================================
const CONFIG = {
defaultVolume: 0.1, // 預設音量 10%
showLogs: true, // 是否顯示除錯紀錄
ui: {
opacityIdle: 0.3, // 平常的透明度 (0~1)
opacityHover: 1.0, // 滑鼠移上去的透明度
positionTop: '10px',
positionRight: '10px', // 靠右設定
color: '#ffffff', // 文字與圖示顏色
bgColor: 'rgba(0, 0, 0, 0.6)' // 背景顏色
}
};
// ==========================================================
function log(msg) {
if (CONFIG.showLogs) {
console.log(`[音量控制] ${msg}`);
}
}
// 注入 CSS 樣式
function injectStyles() {
const styleId = 'meta-volume-fixer-style';
if (document.getElementById(styleId)) return;
const css = `
.mvf-overlay {
position: absolute;
top: ${CONFIG.ui.positionTop};
right: ${CONFIG.ui.positionRight};
z-index: 2147483647; /* 確保在最上層 */
background-color: ${CONFIG.ui.bgColor};
padding: 4px 8px;
border-radius: 20px;
display: flex;
align-items: center;
gap: 8px;
opacity: ${CONFIG.ui.opacityIdle};
transition: opacity 0.2s ease;
font-family: system-ui, -apple-system, sans-serif;
pointer-events: auto !important; /* 強制接收滑鼠事件 */
cursor: default;
isolation: isolate; /* 建立新的堆疊環境 */
}
.mvf-overlay:hover {
opacity: ${CONFIG.ui.opacityHover};
}
.mvf-slider {
-webkit-appearance: none;
width: 80px;
height: 4px;
background: rgba(255, 255, 255, 0.3);
border-radius: 2px;
outline: none;
cursor: pointer;
pointer-events: auto !important;
}
.mvf-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 12px;
height: 12px;
border-radius: 50%;
background: #fff;
cursor: pointer;
transition: transform 0.1s;
pointer-events: auto !important;
}
.mvf-slider::-webkit-slider-thumb:hover {
transform: scale(1.2);
}
.mvf-text {
color: ${CONFIG.ui.color};
font-size: 12px;
font-weight: bold;
min-width: 32px;
text-align: right;
user-select: none;
}
`;
const style = document.createElement('style');
style.id = styleId;
style.textContent = css;
document.head.appendChild(style);
}
/**
* 建立音量控制 UI
* @param {HTMLVideoElement} video
*/
function createVolumeUI(video) {
const parent = video.parentNode;
let container = parent.querySelector('.mvf-overlay');
// 阻擋事件冒泡並阻止預設行為
const stopAndPrevent = (e) => {
e.stopPropagation();
// 阻止預設行為,防止點擊/拖曳滑桿時觸發影片暫停/播放
e.preventDefault();
};
if (!container) {
// 確保父層容器有定位屬性
const parentStyle = window.getComputedStyle(parent);
if (parentStyle.position === 'static') {
parent.style.position = 'relative';
}
container = document.createElement('div');
container.className = 'mvf-overlay';
// 對容器綁定通用事件 (點擊、雙擊、滑鼠抬起/放下)
container.addEventListener('click', stopAndPrevent);
container.addEventListener('dblclick', stopAndPrevent);
container.addEventListener('mouseup', stopAndPrevent);
container.addEventListener('touchend', stopAndPrevent);
container.addEventListener('pointerup', stopAndPrevent);
const slider = document.createElement('input');
slider.type = 'range';
slider.className = 'mvf-slider';
slider.min = '0';
slider.max = '1';
slider.step = '0.01';
const text = document.createElement('span');
text.className = 'mvf-text';
container.appendChild(slider);
container.appendChild(text);
parent.appendChild(container);
// V2.8 修正:將最關鍵的 'mousedown' 和 'touchstart' 直接綁定到滑桿和文字,以最高優先級阻止事件穿透。
slider.addEventListener('mousedown', stopAndPrevent);
slider.addEventListener('touchstart', stopAndPrevent);
slider.addEventListener('pointerdown', stopAndPrevent);
text.addEventListener('mousedown', stopAndPrevent);
text.addEventListener('touchstart', stopAndPrevent);
text.addEventListener('pointerdown', stopAndPrevent);
// 綁定滑桿事件
slider.addEventListener('input', (e) => {
const val = parseFloat(e.target.value);
video.volume = val;
text.textContent = Math.round(val * 100) + '%';
// 標記為使用者透過我們的 UI 手動調整
video.dataset.userManualSet = 'true';
video.dataset.lastVolume = val; // V2.6 新增: 記錄使用者透過自訂滑桿設定的音量
// 如果使用者自己拉到 100%,標記為允許最大音量
video.dataset.userMaxVolume = (val === 1) ? 'true' : 'false';
});
}
// 更新 UI 狀態
const slider = container.querySelector('.mvf-slider');
const text = container.querySelector('.mvf-text');
if (slider && text) {
// 只有當數值真的不同時才更新,避免循環觸發
if (Math.abs(slider.value - video.volume) > 0.01) {
slider.value = video.volume;
text.textContent = Math.round(video.volume * 100) + '%';
}
}
}
/**
* 核心邏輯:強制設定安全音量 (主要用於初始化和自動播放/載入開始)
* @param {HTMLVideoElement} video
* @param {string} reason
*/
function enforceSafeVolume(video, reason) {
// 如果使用者已經透過我們的滑桿手動設定過,則此功能主要檢查是否發生 100% 暴音。
if (video.dataset.userManualSet === 'true') {
const lastVolume = parseFloat(video.dataset.lastVolume) || CONFIG.defaultVolume;
// 例外:如果網站試圖重置為 100%,且使用者未允許 Max Volume,則將其還原至手動設定值
if (video.volume === 1 && video.dataset.userMaxVolume !== 'true') {
log(`[${reason}] 偵測到異常 100% 重置 (手動模式),強制還原至 ${Math.round(lastVolume * 100)}%。`);
video.volume = lastVolume;
}
return;
}
// 執行非手動模式下的強制設定
if (video.volume > CONFIG.defaultVolume) {
video.volume = CONFIG.defaultVolume;
log(`[${reason}] 強制將音量從 ${Math.round(video.volume * 100)}% 修正為 ${CONFIG.defaultVolume * 100}%`);
createVolumeUI(video); // 同步 UI
}
}
/**
* 設定單個影片元素的音量與 UI
* @param {HTMLVideoElement} videoElement
*/
function adjustVolume(videoElement) {
// 1. 初始化設定
if (!videoElement.dataset.mvfInitialized) {
videoElement.dataset.mvfInitialized = 'true';
// 記錄上一次的靜音狀態
videoElement.dataset.lastMuted = videoElement.muted;
// V2.6 新增: 初始化 lastVolume。如果沒設定過,預設為 0.1
if (typeof videoElement.dataset.lastVolume === 'undefined') {
videoElement.dataset.lastVolume = CONFIG.defaultVolume;
}
// 事件 1:音量數值改變
videoElement.addEventListener('volumechange', () => {
// 同步 UI (這必須先執行,確保 UI 反映當前真實音量)
createVolumeUI(videoElement);
// 檢查是否剛解除靜音 (Unmute)
const currentMuted = videoElement.muted;
const lastMuted = videoElement.dataset.lastMuted === 'true';
// 如果剛剛是靜音,現在變成了有聲 (Unmute 事件)
if (lastMuted && !currentMuted) {
// 若網站自動重置,且使用者沒有手動設定過,強制回到預設值 0.1
if (videoElement.dataset.userManualSet !== 'true') {
videoElement.volume = CONFIG.defaultVolume;
log('偵測到解除靜音 (Unmute),強制回到預設音量。');
}
}
// 更新靜音狀態記錄
videoElement.dataset.lastMuted = currentMuted;
// 針對 100% 的絕對防禦 (網站腳本強制重置)
if (videoElement.volume === 1) {
if (videoElement.dataset.userMaxVolume !== 'true') {
// 判斷要還原到預設值 (非手動模式) 還是上次手動設定值 (手動模式)
const resetVolume = (videoElement.dataset.userManualSet === 'true')
? parseFloat(videoElement.dataset.lastVolume) || CONFIG.defaultVolume
: CONFIG.defaultVolume;
videoElement.volume = resetVolume;
log(`攔截到 100% 強制重置 (無論是 Loop 或網站腳本),已還原至 ${Math.round(resetVolume * 100)}%。`);
}
}
});
// 事件 2:開始播放 (針對動態牆回收機制 & 自動播放)
videoElement.addEventListener('play', () => {
// 播放瞬間再次確認 (主要針對非手動模式下的音量初始化)
enforceSafeVolume(videoElement, 'Play Event');
});
// 事件 3:載入新來源 (針對動態牆回收機制)
videoElement.addEventListener('loadstart', () => {
log('偵測到影片來源變更 (Loadstart),重置狀態。');
// 重置所有使用者手動標記,讓新影片從 0.1 開始
videoElement.dataset.userManualSet = 'false';
videoElement.dataset.userMaxVolume = 'false';
// 確保 lastVolume 被設定,以便在 loadstart 時 enforceSafeVolume 可以正確初始化
videoElement.dataset.lastVolume = CONFIG.defaultVolume;
videoElement.dataset.lastMuted = videoElement.muted; // 重置靜音狀態
// 強制設定
setTimeout(() => {
enforceSafeVolume(videoElement, 'Loadstart Init');
}, 0);
});
}
// 2. 確保 UI 存在
createVolumeUI(videoElement);
// 3. 初次執行檢查
enforceSafeVolume(videoElement, 'Init');
}
/**
* 處理頁面上現有的和未來新增的影片
*/
function observePage() {
injectStyles();
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeName === 'VIDEO') {
adjustVolume(node);
} else if (node.nodeType === 1) {
const videos = node.querySelectorAll('video');
videos.forEach(adjustVolume);
}
});
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
document.querySelectorAll('video').forEach(adjustVolume);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', observePage);
} else {
observePage();
}
})();