Greasy Fork

Greasy Fork is available in English.

AbemaTV Volume Control

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

当前为 2023-10-08 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         AbemaTV Volume Control
// @namespace    http://greasyfork.icu/ja/scripts/26397
// @version      17
// @description  ABEMA視聴中にキーボードやマウスホイールで音量を調整します。
// @match        https://abema.tv/
// @match        https://abema.tv/*
// @grant        none
// @license      MIT License
// ==/UserScript==

(() => {
  'use strict';

  const sid = 'VolumeControl',
    ls = JSON.parse(localStorage.getItem(sid) || '{}') || {},
    moConfig = { attributes: true, characterData: true },
    moConfig2 = { childList: true, subtree: true },
    flag = { mute: false, type: 0, vod: false, volume: false, wheel: false },
    interval = { info: 0, init: 0, video: 0, wheel: 0 },
    selector = {
      button: '.com-playback-Volume__icon-button',
      inner: '.c-application-DesktopAppContainer__content',
      marker:
        '.com-tv-TVController__volume .com-a-Slider__marker,.com-vod-VideoControlBar__volume .com-a-Slider__marker,.com-vod-LiveEventPayperviewControlBar__volume .com-a-Slider__marker',
      player:
        '.com-tv-TVScreen__player-container,.com-vod-VODScreen-container,.com-live-event-LiveEventPlayerAreaLayout__player',
      slider:
        '.com-tv-TVController__volume .com-a-Slider,.com-vod-VideoControlBar__volume .com-a-Slider,.com-vod-LiveEventPayperviewControlBar__volume .com-a-Slider',
      sliderH:
        '.com-tv-TVController__volume .com-a-Slider__highlighter,.com-vod-VideoControlBar__volume .com-a-Slider__highlighter,.com-vod-LiveEventPayperviewControlBar__volume .com-a-Slider__highlighter',
      splash: '.com-a-Video__video,.com-live-event__LiveEventPlayerView',
      tv: '.com-tv-TVScreen__player-container',
      video: 'video[src]:not([style*="display: none;"])',
      vod: '.c-vod-EpisodePlayerContainer-container,.com-live-event-LiveEventPlayerSectionLayout__player-area-inner--video',
      vodfull: 'div[class="c-vod-EpisodePlayerContainer-container"]',
      vodfull2: '.com-vod-VODRecommendedContentsContainerView__player > div',
    };
  let observerS;

  /**
   * ページにイベントリスナーを追加
   */
  const addEventPage = () => {
    const id = document.querySelector(`.${sid}_Event`);
    if (!id) {
      log('addEventPage');
      /** @type {HTMLElement|null} */
      const inner = document.querySelector(selector.inner);
      if (inner) {
        inner.classList.add(`${sid}_Event`);
        inner.addEventListener('mousedown', checkMousedown, false);
        inner.addEventListener('wheel', changeVolumeWheel, { passive: true });
      }
    }
  };

  /**
   * 音量を変更できるか判別する
   * @returns {boolean}
   */
  const changeableVolume = () => {
    const vi = document.querySelector(selector.video);
    if (vi && !document.querySelector('.vjs-tech')) {
      flag.type = 2;
      return true;
    }
    flag.type = 0;
    return false;
  };

  /**
   * 動画の音をミュート・解除
   * @param {MouseEvent} e
   */
  const changeMute = (e) => {
    if (e.button === 1 && changeableVolume()) {
      const vi = returnVideo(),
        /** @type {HTMLButtonElement|null} */
        button = document.querySelector(selector.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(String(Math.floor(vi.volume * 100)));
    }
  };

  /**
   * 音量を変更する
   * @param {*} marker ボリュームマーカーの位置
   * @param {*} vol 音量の値
   * @param {boolean} shift Shiftキーを押しているかどうか
   */
  const changeVolume = (marker, vol, shift) => {
    /*
      const info = document.getElementById('VolumeControl_Info'),
        vi = returnVideo(),
        floor2 = (n) => Math.floor(n * 100) / 100;
      let vol, marker;
      flag.vod = false;
      flag.volume = false;
      if (b) {
        flag.volume = true;
        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;
        if (a.path?.length) {
          for (let i = 0; i < a.path.length; i++) {
            if (
              /tv-TVScreen__player|vod-EpisodePlayerContainer/.test(
                a.path[i].className
              )
            ) {
              if (/vod-EpisodePlayerContainer/.test(a.path[i].className)) {
                flag.vod = true;
              }
              flag.volume = true;
              break;
            }
          }
        }
      }
    */
    const floor2 = (n) => Math.floor(n * 100) / 100,
      info = document.getElementById(`${sid}_Info`),
      pl2 = document.querySelector(selector.vodfull2),
      full = document.querySelector(selector.vodfull)
        ? true
        : pl2 && getComputedStyle(pl2, '::backdrop').position === 'fixed'
        ? true
        : false;
    if (
      info &&
      flag.volume &&
      (!flag.vod || (flag.vod && (full || (!full && shift))))
    ) {
      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, 'mouseup');
      }, 150);
      moveVolumeMarker(marker, 'mousedown');
    }
  };

  /**
   * キーボードで音量を変更する
   * @param {number} a 音量の変更量
   */
  const changeVolumeKeyboard = (a) => {
    if (changeableVolume()) {
      const vi = returnVideo(),
        floor2 = (n) => Math.floor(n * 100) / 100;
      flag.volume = true;
      changeVolume(a * 100, vi ? floor2(vi.volume) + a / -1 : 0, false);
    } else log('changeVolumeKeyboard: not changeableVolume');
  };

  /**
   * マウスホイールで音量を変更する
   * @param {WheelEvent} e
   */
  const changeVolumeWheel = (e) => {
    if (changeableVolume() && e.target instanceof HTMLElement) {
      const y = e.deltaMode > 0 ? Math.round(e.deltaY) * 100 : e.deltaY,
        vi = returnVideo(),
        floor2 = (n) => Math.floor(n * 100) / 100,
        tv = document.querySelector(selector.tv),
        vod = document.querySelector(selector.vod);
      flag.vod = false;
      flag.volume = false;
      if (tv?.contains(e.target)) {
        flag.volume = true;
      } else if (vod?.contains(e.target)) {
        flag.vod = true;
        flag.volume = true;
      }
      changeVolume(
        e.deltaMode > 0 ? Math.round(e.deltaY) : e.deltaY / 100,
        vi ? floor2(vi.volume) + y / -10000 : 0,
        e.shiftKey
      );
    } else log('changeVolumeWheel: not changeableVolume');
  };

  /**
   * 動画を構成している要素に変更があったとき
   */
  const checkChangeElements = () => {
    const inner = document.querySelector(selector.inner);
    if (inner) {
      setTimeout(() => {
        addEventPage();
        checkVolumeSliderObserve();
      }, 50);
    }
  };

  /**
   * キーボードのキーを押したとき
   * @param {KeyboardEvent} e
   */
  const checkKeyDown = (e) => {
    if (
      !(
        e.target instanceof HTMLInputElement ||
        e.target instanceof HTMLTextAreaElement
      )
    ) {
      if (!e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
        if (e.key === 'ArrowUp') {
          e.stopPropagation();
          changeVolumeKeyboard(-0.05);
        } else if (e.key === 'ArrowDown') {
          e.stopPropagation();
          changeVolumeKeyboard(0.05);
        }
      }
    }
  };

  /**
   * マウスのボタンを押したとき
   * @param {MouseEvent} e
   */
  const checkMousedown = (e) => {
    if (e.button === 1) {
      if (e.target instanceof HTMLElement) {
        const player = document.querySelector(selector.player);
        if (player?.contains(e.target)) {
          e.preventDefault();
          changeMute(e);
        }
      }
    }
  };

  /**
   * 音量スライダーが監視されていなければ監視する
   */
  const checkVolumeSliderObserve = () => {
    const id = document.querySelector(`.${sid}_Slider`);
    if (!id) {
      log('checkVolumeSliderObserve');
      const eSlider = document.querySelector(selector.sliderH);
      if (eSlider) {
        eSlider.classList.add(`${sid}_Slider`);
        observerS.observe(eSlider, moConfig);
      } else log('checkVolumeSliderObserve: Not found element.', 'error');
    }
  };

  /**
   * 音量を表示する要素を作成
   */
  const createInfo = () => {
    const css = `
      #${sid}_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;
        user-select: none;
        visibility: hidden;
        z-index: 2260;
      }
      #${sid}_Info.vc_show {
        opacity: 0.8;
        visibility: visible;
      }
      #${sid}_Info.vc_hidden {
        opacity: 0;
        transition: opacity 0.5s ease-out, visibility 0.5s ease-out;
        visibility: hidden;
      }
      #${sid}_Info span:before,
      #${sid}_Info span:after {
        box-sizing: content-box !important;
      }
      .vc_icon_before_hidden #${sid}_Volume2::before,
      .vc_icon_after_hidden #${sid}_Volume2::after {
        visibility: hidden;
      }
      #${sid}_Info span::before,
      #${sid}_Info span::after {
        content: '';
        display: block;
        position: absolute;
      }
      #${sid}_Volume1 {
        height: 20px;
        position: relative;
        width: 30px;
      }
      #${sid}_Volume1::before {
        background: #fff;
        height: 8px;
        left: 2px;
        top: 6px;
        width: 4px;
      }
      #${sid}_Volume1::after {
        border: 5px transparent solid;
        border-left-width: 0;
        border-right-color: #fff;
        height: 8px;
        left: 6px;
        top: 1px;
        width: 0;
      }
      #${sid}_Volume2,
      #${sid}_Volume3 {
        position: absolute;
      }
      #${sid}_Volume2 {
        left: 8px;
        top: 5px;
      }
      #${sid}_Volume2::before,
      #${sid}_Volume2::after {
        border: 2px solid transparent;
        border-right: 2px solid #fff;
      }
      #${sid}_Volume2::before {
        border-radius: 20px;
        height: 20px;
        left: -3px;
        top: -2px;
        width: 20px;
      }
      #${sid}_Volume2::after {
        border-radius: 10px;
        height: 15px;
        left: -2px;
        top: 1px;
        width: 15px;
      }
      #${sid}_Volume3 {
        left: 20px;
        top: 14px;
      }
      #${sid}_Volume3::before,
      #${sid}_Volume3::after {
        background-color: #fff;
        height: 2px;
        width: 12px;
      }
      #${sid}_Volume3::before {
        transform: rotate(45deg);
      }
      #${sid}_Volume3::after {
        transform: rotate(135deg);
      }
      #${sid}_Volume4 {
        font-weight: bold;
        margin-left: 1ex;
      }
      `,
      div = document.createElement('div'),
      style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);
    div.id = `${sid}_Info`;
    div.innerHTML = `
      <span id="${sid}_Volume1"></span>
      <span id="${sid}_Volume2"></span>
      <span id="${sid}_Volume3"></span>
      <span id="${sid}_Volume4"></span>
      `;
    document.body.appendChild(div);
  };

  /**
   * ページを開いたときに1度だけ実行
   */
  const init = () => {
    log('init');
    observerS = new MutationObserver(changeSlider);
    waitShowVideo();
    createInfo();
  };

  /**
   * デバッグ用ログ
   * @param {...any} a
   */
  const log = (...a) => {
    if (ls.debug) {
      try {
        if (/^debug$|^error$|^info$|^warn$/.test(a[a.length - 1])) {
          const b = a.pop();
          console[b](sid, a.join('  '));
          showInfo(a[0]);
        } else console.log(sid, a.join('  '));
      } catch (e) {
        if (e instanceof Error) console.error(e.message, ...a);
        else if (typeof e === 'string') console.error(e, ...a);
        else console.error('log error', ...a);
      }
    }
  };

  /**
   * ボリュームスライダーのマーカーを動かして音量を変更する
   * @param {number} n ボリュームスライダーのマーカー位置
   * @param {string} type mouseupかmousedown
   */
  const moveVolumeMarker = (n, type) => {
    const slider = document.querySelector(selector.slider),
      marker = document.querySelector(selector.marker);
    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要素を返す
   * @returns {HTMLVideoElement|null}
   */
  const returnVideo = () => {
    if (flag.type === 2) {
      /** @type {HTMLVideoElement|null} */
      const vi = document.querySelector(selector.video);
      if (vi) return vi;
    }
    return null;
  };

  /**
   * 現在の音量を表示
   * @param {string} s 表示する文字列
   */
  const showInfo = (s) => {
    const eInfo = document.getElementById(`${sid}_Info`),
      eVol2 = document.getElementById(`${sid}_Volume2`),
      eVol3 = document.getElementById(`${sid}_Volume3`),
      eVol4 = document.getElementById(`${sid}_Volume4`),
      vi = returnVideo();
    if (eVol4) eVol4.textContent = vi?.muted ? 'ミュート' : s ? s : '';
    if (eVol2 && eVol3) {
      if (vi?.muted) {
        eVol2.style.display = 'none';
        eVol3.style.display = 'block';
      } else {
        eVol2.style.display = 'block';
        eVol3.style.display = 'none';
      }
    }
    if (eInfo) {
      eInfo.classList.remove('vc_hidden');
      eInfo.classList.add('vc_show');
    }
    clearTimeout(interval.info);
    interval.info = setTimeout(() => {
      if (eInfo) {
        eInfo.classList.remove('vc_show');
        eInfo.classList.add('vc_hidden');
      }
    }, 1000);
  };

  /**
   * 指定時間だけ待つ
   * @param {number} msec 待ち時間
   */
  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(selector.splash);
      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();
      const vi = returnVideo();
      if (vi && !isNaN(vi.duration) && splash()) {
        clearInterval(interval.video);
        startFirstObserve();
      }
    }, 250);
  };

  const observerC = new MutationObserver(checkChangeElements);
  clearInterval(interval.init);
  interval.init = setInterval(() => {
    if (
      /^https:\/\/abema\.tv\/(?:now-on-air|video\/episode|live-event)\/.+$/.test(
        location.href
      )
    ) {
      clearInterval(interval.init);
      init();
    }
  }, 1000);
})();