Greasy Fork

Greasy Fork is available in English.

AbemaTV Volume Control

ABEMA閲覧中にキーボードやマウスホイールで音量を調整します。

目前为 2021-12-04 提交的版本。查看 最新版本

// ==UserScript==
// @name         AbemaTV Volume Control
// @namespace    http://greasyfork.icu/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
// ==/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 = `
      <span id="VolumeControl_Volume1"></span>
      <span id="VolumeControl_Volume2"></span>
      <span id="VolumeControl_Volume3"></span>
      <span id="VolumeControl_Volume4"></span>
      `;
    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);
})();