// ==UserScript==
// @name AbemaTV Volume Control
// @namespace https://greasyfork.org/ja/scripts/26397
// @version 15
// @description ABEMA閲覧中にキーボードやマウスホイールで音量を調整します。
// @include /^https:\/\/([a-z0-9-]+\.)?abema\.tv\/(now-on-air\/[a-z0-9-]+)?$/
// @grant none
// @license MIT License
// @downloadURL none
// ==/UserScript==
(() => {
'use strict';
const sid = 'VolumeControl',
selectorInner = '.c-tv-NowOnAirContainer__inner',
selectorVideo = 'video[style*="display: block;"]',
ls = JSON.parse(localStorage.getItem(sid)) || {},
moConfig = { attributes: true, characterData: true },
moConfig2 = { childList: true, subtree: true },
flag = { mute: false, type: 0, wheel: false },
interval = { info: 0, init: 0, video: 0, wheel: 0 };
let observerS;
//ページにイベントリスナーを追加
const addEventPage = () => {
const id = document.querySelector(`.${sid}_Event`);
if (!id) {
log('addEventPage');
const inner = document.querySelector(selectorInner);
if (inner) {
inner.classList.add(`${sid}_Event`);
inner.addEventListener('mousedown', checkMousedown, false);
inner.addEventListener('wheel', changeVolume, { passive: true });
}
}
};
//音量を変更できるか判別する
const changeableVolume = () => {
if (
window.theoplayer &&
theoplayer.player &&
theoplayer.player(0) &&
Object.prototype.hasOwnProperty.call(theoplayer.player(0), 'volume')
) {
flag.type = 1;
return true;
}
const vi = document.querySelector(selectorVideo);
if (vi && !document.querySelector('.vjs-tech')) {
flag.type = 2;
return true;
}
flag.type = 0;
return false;
};
//動画の音をミュート・解除
const changeMute = (e) => {
if (e.button === 1 && changeableVolume()) {
const vi = returnVideo(),
button = document.querySelector('.com-playback-Volume__icon-button');
if (vi) {
if (button) button.click();
if (vi.muted) showInfo();
else showInfo(String(Math.floor(vi.volume * 100)));
}
}
};
//音量スライダーの位置が動いたとき
const changeSlider = () => {
const vi = returnVideo();
if (vi) {
if (vi.muted) showInfo();
else showInfo(Math.floor(vi.volume * 100));
}
};
//音量を変更する
const changeVolume = (a, b) => {
if (changeableVolume()) {
const info = document.getElementById('VolumeControl_Info'),
vi = returnVideo(),
floor2 = (n) => Math.floor(n * 100) / 100;
let vol, marker;
if (b) {
vol = floor2(vi.volume) + a / -1;
marker = a * 100;
} else {
const y = a.deltaMode > 0 ? Math.round(a.deltaY) * 100 : a.deltaY;
marker = a.deltaMode > 0 ? Math.round(a.deltaY) : a.deltaY / 100;
vol = floor2(vi.volume) + y / -10000;
}
vol = vol > 1 ? 1 : vol < 0 ? 0 : vol;
vol = floor2(vol);
if (vol > 0.66) {
info.classList.remove('vc_icon_before_hidden');
info.classList.remove('vc_icon_after_hidden');
} else if (vol > 0.33) {
info.classList.add('vc_icon_before_hidden');
info.classList.remove('vc_icon_after_hidden');
} else {
info.classList.add('vc_icon_before_hidden');
info.classList.add('vc_icon_after_hidden');
}
clearTimeout(interval.wheel);
flag.wheel = true;
interval.wheel = setTimeout(() => {
flag.wheel = false;
moveVolumeMarker(marker, true);
}, 150);
moveVolumeMarker(marker);
} else log('changeVolume: not changeableVolume');
};
//動画を構成している要素に変更があったとき
const checkChangeElements = () => {
const inner = document.querySelector(selectorInner);
if (inner) {
setTimeout(() => {
addEventPage();
checkVolumeSliderObserve();
}, 50);
}
};
//キーボードのキーを押したとき
const checkKeyDown = (e) => {
if (/input|textarea/i.test(e.target.tagName)) return;
if (!e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
if (e.key === 'ArrowUp' || e.keyCode === 38) {
e.stopPropagation();
changeVolume(-0.05, 2);
} else if (e.key === 'ArrowDown' || e.keyCode === 40) {
e.stopPropagation();
changeVolume(0.05, 2);
}
}
};
//マウスのボタンを押したとき
const checkMousedown = (e) => {
if (e.button === 1) changeMute(e);
};
//音量スライダーが監視されていなければ監視する
const checkVolumeSliderObserve = () => {
const id = document.querySelector(`.${sid}_Slider`);
if (!id) {
log('checkVolumeSliderObserve');
const eSlider = document.querySelector('.com-a-Slider__highlighter');
if (eSlider) {
eSlider.classList.add(`${sid}_Slider`);
observerS.observe(eSlider, moConfig);
} else log('checkVolumeSliderObserve: Not found element.', 'error');
}
};
//音量を表示する要素を作成
const createInfo = () => {
const css = `
#VolumeControl_Info {
align-items: center;
background-color: rgba(0, 0, 0, 0.4);
border-radius: 4px;
bottom: 70px;
color: #fff;
display: flex;
justify-content: center;
left: 90px;
min-height: 30px;
min-width: 3em;
opacity: 0;
padding: 0.5ex 1ex;
position: fixed;
visibility: hidden;
z-index: 2260;
}
#VolumeControl_Info.vc_show {
opacity: 0.8;
visibility: visible;
}
#VolumeControl_Info.vc_hidden {
opacity: 0;
transition: opacity 0.5s ease-out, visibility 0.5s ease-out;
visibility: hidden;
}
#VolumeControl_Info span:before,
#VolumeControl_Info span:after {
box-sizing: content-box !important;
}
.vc_icon_before_hidden #VolumeControl_Volume2::before,
.vc_icon_after_hidden #VolumeControl_Volume2::after {
visibility: hidden;
}
#VolumeControl_Info span::before,
#VolumeControl_Info span::after {
content: '';
display: block;
position: absolute;
}
#VolumeControl_Volume1 {
height: 20px;
position: relative;
width: 30px;
}
#VolumeControl_Volume1::before {
background: #fff;
height: 8px;
left: 2px;
top: 6px;
width: 4px;
}
#VolumeControl_Volume1::after {
border: 5px transparent solid;
border-left-width: 0;
border-right-color: #fff;
height: 8px;
left: 6px;
top: 1px;
width: 0;
}
#VolumeControl_Volume2,
#VolumeControl_Volume3 {
position: absolute;
}
#VolumeControl_Volume2 {
left: 8px;
top: 5px;
}
#VolumeControl_Volume2::before,
#VolumeControl_Volume2::after {
border: 2px solid transparent;
border-right: 2px solid #fff;
}
#VolumeControl_Volume2::before {
border-radius: 20px;
height: 20px;
left: -3px;
top: -2px;
width: 20px;
}
#VolumeControl_Volume2::after {
border-radius: 10px;
height: 15px;
left: -2px;
top: 1px;
width: 15px;
}
#VolumeControl_Volume3 {
left: 20px;
top: 14px;
}
#VolumeControl_Volume3::before,
#VolumeControl_Volume3::after {
background-color: #fff;
height: 2px;
width: 12px;
}
#VolumeControl_Volume3::before {
transform: rotate(45deg);
}
#VolumeControl_Volume3::after {
transform: rotate(135deg);
}
#VolumeControl_Volume4 {
font-weight: bold;
margin-left: 1ex;
}
`,
div = document.createElement('div'),
style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
div.id = 'VolumeControl_Info';
div.innerHTML = `
`;
document.body.appendChild(div);
};
//ページを開いたときに1度だけ実行
const init = () => {
log('init');
observerS = new MutationObserver(changeSlider);
waitShowVideo();
createInfo();
};
//デバッグ用 ログ
const log = (...a) => {
if (ls.debug) {
if (/^debug$|^error$|^info$|^warn$/.test(a[a.length - 1])) {
const b = a.pop();
console[b](sid, a.toString());
showInfo(a[0]);
} else console.log(sid, a.toString());
}
};
//ボリュームスライダーのマーカーを動かして音量を変更する
const moveVolumeMarker = (n, b) => {
const slider = document.querySelector('.com-a-Slider'),
marker = document.querySelector('.com-a-Slider__marker'),
type = b ? 'mouseup' : 'mousedown';
if (n && slider && marker) {
slider.dispatchEvent(
new MouseEvent(type, {
bubbles: true,
cancelable: true,
view: window,
clientX: marker.getBoundingClientRect().x,
clientY: marker.getBoundingClientRect().y + n + 5,
})
);
}
};
//video要素を返す
const returnVideo = () => {
if (flag.type === 1) return theoplayer.player(0);
if (flag.type === 2) {
const vi = document.querySelector(selectorVideo);
if (vi) return vi;
}
return null;
};
//現在の音量を表示
const showInfo = (s) => {
const eInfo = document.getElementById('VolumeControl_Info'),
eVol2 = document.getElementById('VolumeControl_Volume2'),
eVol3 = document.getElementById('VolumeControl_Volume3'),
eVol4 = document.getElementById('VolumeControl_Volume4'),
vi = returnVideo();
eVol4.textContent = vi?.muted ? 'ミュート' : s ? s : '';
if (vi?.muted) {
eVol2.style.display = 'none';
eVol3.style.display = 'block';
} else {
eVol2.style.display = 'block';
eVol3.style.display = 'none';
}
eInfo.classList.remove('vc_hidden');
eInfo.classList.add('vc_show');
clearTimeout(interval.info);
interval.info = setTimeout(() => {
eInfo.classList.remove('vc_show');
eInfo.classList.add('vc_hidden');
}, 1000);
};
//指定時間だけ待つ
const sleep = (msec) => new Promise((resolve) => setTimeout(resolve, msec));
//ページを開いて動画が表示されたら1度だけ実行
const startFirstObserve = () => {
log('startFirstObserve');
addEventPage();
document.addEventListener('keydown', checkKeyDown, true);
const main = document.querySelector('main');
if (main) observerC.observe(main, moConfig2);
else log('startFirstObserve: Not found element.', 'error');
checkVolumeSliderObserve();
};
//動画が表示されるのを待つ
const waitShowVideo = async () => {
log('waitShowVideo');
const splash = () => {
const sp = document.querySelector('.com-a-Video__video');
if (!sp) {
log('waitShowVideo: Not found element.', 'error');
return true;
}
const cs = getComputedStyle(sp);
if (cs?.visibility === 'visible') return true;
return false;
};
await sleep(400);
clearInterval(interval.video);
interval.video = setInterval(() => {
changeableVolume();
if (returnVideo() && !isNaN(returnVideo().duration) && splash()) {
clearInterval(interval.video);
startFirstObserve();
}
}, 250);
};
const observerC = new MutationObserver(checkChangeElements);
clearInterval(interval.init);
interval.init = setInterval(() => {
if (
/^https:\/\/([a-z0-9-]+\.)?abema\.tv\/now-on-air\/[a-z0-9-]+$/.test(
location.href
)
) {
clearInterval(interval.init);
init();
}
}, 1000);
})();