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