Greasy Fork is available in English.

AbemaTV Volume Control

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

Ajankohdalta 4.8.2017. Katso uusin versio.

// ==UserScript==
// @name         AbemaTV Volume Control
// @namespace    https://greasyfork.org/ja/scripts/26397
// @version      2
// @description  AbemaTV(HTML5版)閲覧中にキーボードやマウスホイールで音量を調整します。
// @include      https://abema.tv/now-on-air/*
// @grant        GM_addStyle
// ==/UserScript==

(function() {
  'use strict';


  /* ---------- Settings ---------- */

  // 変更した値はブラウザのローカルストレージに保存するので
  // スクリプトをバージョンアップするたびに書き換える必要はありません。
  // (値が0のとき、以前に変更した値か初期値を使用します)

  // ページ右下の音量ボタン&音量ゲージを表示する
  //(音量ゲージ操作で変更した音量と当スクリプトで変更した音量は同期できない場合があります)
  // 1:表示する / 2:表示しない
  // 初期値:2
  // 有効値:1 ~ 2
  var showVolumeButton = 0;

  /* ------------------------------ */

  //THEOplayerのプレイヤーもしくはaudio要素にイベントリスナーを追加
  function addEventAudio(s) {
    log(['addEventAudio', s], 'debug');
    if (flag.type === 1) {
      if (theoplayer.player && !theoplayer.player(0).element.classList.contains(sid)) {
        theoplayer.player(0).element.classList.add(sid);
        theoplayer.player(0).addEventListener('volumechange', audioVolumeChange, false);
        if (ls.debug) {
          log3(theoplayer.player(0), 'canplay');
          log3(theoplayer.player(0), 'canplaythrough');
          log3(theoplayer.player(0), 'durationchange');
          log3(theoplayer.player(0), 'emptied');
          log3(theoplayer.player(0), 'ended');
          log3(theoplayer.player(0), 'error');
          log3(theoplayer.player(0), 'loadeddata');
          log3(theoplayer.player(0), 'loadedmetadata');
          log3(theoplayer.player(0), 'loadstart');
          log3(theoplayer.player(0), 'pause');
          log3(theoplayer.player(0), 'play');
          log3(theoplayer.player(0), 'playing');
          log3(theoplayer.player(0), 'seeked');
          log3(theoplayer.player(0), 'seeking');
          log3(theoplayer.player(0), 'stalled');
          log3(theoplayer.player(0), 'suspend');
          log3(theoplayer.player(0), 'waiting');
        }
      }
    } else if (flag.type === 2) {
      var au = document.getElementsByTagName('audio');
      for (var i = 0, j = au.length; i < j; i++) {
        if (!au[i].classList.contains(sid)) {
          au[i].classList.add(sid);
          au[i].addEventListener('emptied', audioEmptied, false);
          au[i].addEventListener('ended', audioEnded, false);
          au[i].addEventListener('loadstart', audioLoadstart, false);
          au[i].addEventListener('play', audioPlay, false);
          au[i].addEventListener('volumechange', audioVolumeChange, false);
          if (ls.debug) {
            log3(au[i], 'canplay');
            log3(au[i], 'canplaythrough');
            log3(au[i], 'durationchange');
            log3(au[i], 'error');
            log3(au[i], 'loadeddata');
            log3(au[i], 'loadedmetadata');
            log3(au[i], 'loadstart');
            log3(au[i], 'pause');
            log3(au[i], 'playing');
            log3(au[i], 'seeked');
            log3(au[i], 'seeking');
            log3(au[i], 'stalled', true);
            log3(au[i], 'suspend');
            log3(au[i], 'waiting');
          }
        }
      }
    }
  }

  //ページにイベントリスナーを追加
  function addEventPage() {
    log('addEventPage');
    var o = document.querySelector('div[class^="style__overlap___"]');
    if (o) {
      o.addEventListener('mousedown', checkMousedown, false);
      o.addEventListener('wheel', changeVolume, false);
    }
    if (showVolumeButton === 1) {
      var v = document.querySelector('div[class^="styles__volume"] button');
      if (v) v.addEventListener('click', clickVolumeButton, false);
    }
    document.addEventListener('keydown', checkKeyDown, true);
  }

  //動画が空になったとき
  function audioEmptied() {
    log2('audioEmptied', 'debug');
    if (!flag.mute && !flag.wheel) checkVolume('audioEmptied');
  }

  //動画を最後まで再生したとき
  function audioEnded() {
    log2('audioEnded', 'debug');
    if (!flag.mute && !flag.wheel) checkVolume('audioEnded');
  }

  //動画をこれから読み込むとき
  function audioLoadstart() {
    log2('audioLoadstart', 'debug');
    if (!flag.mute && !flag.wheel) checkVolume('audioLoadstart');
  }

  //動画を再生し始めたとき
  function audioPlay() {
    log2('audioPlay', 'debug');
    if (!flag.mute && !flag.wheel) checkVolume('audioPlay');
  }

  //動画のボリュームが変わったとき
  function audioVolumeChange() {
    log2('audioVolumeChange', 'debug');
    if (!flag.mute && !flag.wheel) checkVolume('audioVolumeChange');
  }

  //音量を変更できるか判別する
  function changeableVolume() {
    log('changeableVolume');
    if (theoplayer && theoplayer.player && theoplayer.player(0) && theoplayer.player(0).hasOwnProperty('volume')) {
      flag.type = 1;
      return true;
    }
    var vi = document.getElementsByTagName('video'),
      au = document.getElementsByTagName('audio');
    if (au.length > 0 && vi.length === au.length && !document.getElementsByClassName('vjs-tech').length) {
      flag.type = 2;
      return true;
    }
    flag.type = 0;
    return false;
  }

  //audio要素の値を変更する
  function changeAudioElementsValue(n, v) {
    log('changeAudioElementsValue');
    if (flag.type === 1) theoplayer.player(0)[n] = v;
    else if (flag.type === 2) {
      var au = document.getElementsByTagName('audio');
      for (var i = 0, j = au.length; i < j; i++) {
        au[i][n] = v;
      }
    }
  }

  //動画の音をミュート・解除
  function changeMute(a, b) {
    log(['changeMute', a, b]);
    if ((!a && b) || (a.button === 1) && changeableVolume()) {
      var au = returnAudio();
      if (!au) return;
      if (au.muted) {
        ls.muted = false;
        changeAudioElementsValue('volume', ls.beforeMute);
      } else {
        ls.muted = true;
        ls.beforeMute = au.volume;
      }
      saveLocalStorage();
      changeAudioElementsValue('muted', !au.muted);
      if (au.muted) showInfo();
      else showInfo(String(Math.round(au.volume * 100)));
      if (showVolumeButton === 1 && !flag.mute) {
        switchVolumeButtonImage();
      }
    }
  }

  //番組名が変更したとき
  function changeProgramTitle() {
    log('changeProgramTitle', 'debug');
    if (changeableVolume() && returnAudio()) {
      checkVolume('changeProgramTitle1');
      addEventAudio('changeProgramTitle1');
    } else {
      clearInterval(interval.channel);
      interval.channel = setInterval(function() {
        if (changeableVolume() && returnAudio()) {
          clearInterval(interval.channel);
          checkVolume('changeProgramTitle2');
          addEventAudio('changeProgramTitle2');
        }
      }, 200);
    }
  }

  //ボリュームスライダーの位置が動いたとき
  function changeSlider() {
    log2(['changeSlider', flag.wheel]);
    if (!flag.mute && !flag.wheel) changeVolume(returnAudio().volume, 1);
  }

  //音量を変更する
  function changeVolume(a, b) {
    if (changeableVolume()) {
      log2(['changeVolume1', a, b]);
      var info = document.getElementById('VolumeControl_Info'),
        au = returnAudio(),
        vol;
      if (b) vol = a;
      else {
        var y = (a.deltaMode > 0) ? a.deltaY * 100 : a.deltaY;
        if (au.volume === 1 || au.volume === 0) vol = ls.volume + (y / -10000);
        else vol = ((au.muted || ls.muted) ? ls.beforeMute : au.volume) + (y / -10000);
      }
      vol = (vol > 1) ? 1 : (vol < 0) ? 0 : 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(function() {
        flag.wheel = false;
      }, 150);
      if (showVolumeButton === 1 && eSlider) eSlider.style.height = Math.ceil(vol * 92) + 'px';
      if ((au.muted && b !== 3)) {
        changeMute(null, true);
        if (showVolumeButton === 1 && eSlider) switchVolumeButtonImage();
      }
      if (!au.muted && ls.muted) {
        changeAudioElementsValue('muted', true);
      }
      ls.volume = vol;
      saveLocalStorage();
      changeAudioElementsValue('volume', vol);
      if (b !== 1) showInfo(String(Math.round(vol * 100)));
    } else log('changeVolume2');
  }

  //動画を構成している要素に変更があったとき
  function checkChangeElements() {
    if (flag.type === 2) addEventAudio('checkChangeElement');
  }

  //キーボードのキーを押したとき
  function checkKeyDown(e) {
    if (/input|textarea/i.test(e.target.tagName)) return;
    if (!e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey && changeableVolume()) {
      var v = returnAudio().volume;
      if (e.keyCode === 38) {
        e.stopPropagation();
        v += 0.05;
        if (v > 1) v = 1;
        changeVolume(v, 2);
      } else if (e.keyCode === 40) {
        e.stopPropagation();
        v -= 0.05;
        if (v < 0) v = 0;
        changeVolume(v, 2);
      } else if (e.keyCode === 77) changeMute(null, true);
    }
  }

  //マウスのボタンを押したとき
  function checkMousedown(e) {
    if (e.button === 1) changeMute(e);
  }

  //ボリュームの値を調べる
  function checkVolume(s) {
    log(['checkVolume', s]);
    var au = returnAudio();
    if (au && au.muted) showInfo();
    if (au && au.volume !== ls.volume) {
      log2(['checkVolume', s]);
      restoreVolume();
    }
    if (au && au.muted !== ls.muted) changeMute(null, true);
  }

  //ボリュームボタンをクリックしたとき
  function clickVolumeButton(e) {
    log2('clickVolumeButton');
    if (!flag.mute) {
      e.stopPropagation();
      flag.mute = true;
      clearTimeout(interval.mute);
      interval.mute = setTimeout(function() {
        flag.mute = false;
      }, 150);
      changeMute(null, true);
      switchVolumeButtonImage();
    } else {
      if (returnAudio().volume && ls.volume) ls.beforeMute = ls.volume;
      else changeVolume(ls.beforeMute, 1);
    }
  }

  //音量を表示する要素を作成
  function createInfo() {
    var style = '#VolumeControl_Info {align-items:center; background-color:rgba(0,0,0,0.4); border-radius:4px; bottom:80px; color:#FFF; display:flex; justify-content:center; left:20px; min-height:30px; min-width:3em; opacity:0; padding:0.5ex 1ex; position:fixed; visibility:hidden;}' +
      '#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 {top:5px; left:8px;}' +
      '#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');
    GM_addStyle(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度だけ実行
  function init() {
    log('init');
    setupSettings();
    if (showVolumeButton === 1) observerS = new MutationObserver(changeSlider);
    waitShowVideo();
    createInfo();
  }

  //デバッグ用 ログ
  function log(s, t) {
    if (ls.debug) {
      if (t) console[t](sid, s);
      else console.log(sid, s);
    }
  }

  //デバッグ用 ログ2
  function log2(s, t) {
    if (ls.debug) {
      var fl = function(num) {
        if (isFinite(num)) return Math.floor(Number(num) * 1000) / 1000;
        return num;
      };
      var au = returnAudio(),
        a = [fl(ls.beforeMute), ls.muted, fl(ls.volume)];
      if (au) a.push(au.muted, fl(au.volume));
      if (Array.isArray(s)) {
        for (var i = s.length; i > 0; i--) {
          a.unshift(s[i - 1]);
        }
      } else a.unshift(s);
      log(a, t);
    }
  }

  //デバッグ用 ログ3
  function log3(e, a, b) {
    if (ls.debug) {
      if (a === 'stalled' && b) {
        e.addEventListener(a, function() {
          var au1 = document.getElementsByTagName('audio');
          for (var i1 = 0, j1 = au1.length; i1 < j1; i1++) {
            if (!au1[i1].classList.contains(sid)) log2(['stalled not add event', i1], 'debug');
          }
        }, false);
      } else {
        e.addEventListener(a, function() { log2(a, 'debug'); }, false);
      }
    }
  }

  //以前調整した音量を復元する
  function restoreVolume() {
    log('restoreVolume');
    if (changeableVolume() && ls.volume >= 0) changeVolume(ls.volume, 3);
  }

  //THEOplayerのプレイヤーもしくはaudio要素を返す
  function returnAudio() {
    if (flag.type === 1) return theoplayer.player(0);
    if (flag.type === 2) {
      var vi = document.getElementsByTagName('video'),
        au = document.getElementsByTagName('audio'),
        n = -1;
      for (var i = 0, j = vi.length; i < j; i++) {
        if (vi[i].src && vi[i].style.display !== 'none') {
          n = i;
          break;
        }
      }
      if (n >= 0 && au[n].src) return au[n];
      for (var k = 0, l = au.length; k < l; k++) {
        if (au[k].src) return au[k];
      }
    }
    return null;
  }

  //THEOplayerのプレイヤーもしくはvideo要素を返す
  function returnVideo() {
    if (flag.type === 1) return theoplayer.player(0);
    if (flag.type === 2) {
      var vi = document.getElementsByTagName('video');
      for (var i = 0, j = vi.length; i < j; i++) {
        if (vi[i].src && vi[i].style.display !== 'none') return vi[i];
      }
    }
    return null;
  }

  //ローカルストレージに設定を保存する
  function saveLocalStorage() {
    localStorage.setItem(sid, JSON.stringify(ls));
  }

  //設定の値を用意する
  function setupSettings() {
    var vb = (Number.isInteger(Number(showVolumeButton))) ? Number(showVolumeButton) : 0;
    vb = (vb === 0) ? 0 : (vb > 2) ? 2 : (vb < 1) ? 1 : vb;
    showVolumeButton = (ls.showVolumeButton) ? ls.showVolumeButton : (vb) ? vb : 2;
    if (vb && ls.showVolumeButton !== vb) {
      showVolumeButton = vb;
      ls.showVolumeButton = vb;
      saveLocalStorage();
    }
    if (isNaN(Number(ls.volume))) ls.volume = -1;
    else if (ls.volume >= 0) {
      localStorage.setItem('abm_volume', ls.volume);
      localStorage.setItem('volume', ls.volume);
    }
    if (isNaN(Number(ls.beforeMute))) ls.beforeMute = -1;
  }

  //現在の音量を表示
  function showInfo(s) {
    var eInfo = document.getElementById('VolumeControl_Info'),
      eVol2 = document.getElementById('VolumeControl_Volume2'),
      eVol3 = document.getElementById('VolumeControl_Volume3'),
      eVol4 = document.getElementById('VolumeControl_Volume4'),
      au = returnAudio();
    eVol4.textContent = (au && au.muted) ? 'ミュート' : (s) ? s : '';
    if (au && au.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(function() {
      eInfo.classList.remove('vc_show');
      eInfo.classList.add('vc_hidden');
    }, 1000);
  }

  //ページを開いて動画が表示されたら1度だけ実行
  function startObserve() {
    log('startObserve');
    addEventAudio('startObserve');
    addEventPage();
    restoreVolume();
    observerT.observe(document.querySelector('span[class^="styles__title___"]'), moConfig2);
    observerC.observe(document.querySelector('div[class^="styles__tv-container___"]'), moConfig3);
    if (showVolumeButton === 1) {
      eSlider = document.querySelector('div[class^="styles__highlighter___"]');
      observerS.observe(eSlider, moConfig);
      switchVolumeButtonImage();
    } else {
      var eVolume = document.querySelector('div[class^="styles__volume___"]');
      if (eVolume) eVolume.style.display = 'none';
    }
  }

  //ボリュームボタンの画像を切り替える
  function switchVolumeButtonImage() {
    log('switchVolumeButtonImage');
    var use = document.querySelector('div[class^="styles__volume"] use'),
      href;
    if (use && use.hasAttribute('xlink:href')) {
      href = use.getAttribute('xlink:href');
      if (href) {
        if (returnAudio().muted) href = href.replace(/^(.+volume_)on$/, '$1off');
        else href = href.replace(/^(.+volume_)off$/, '$1on');
        use.setAttribute('xlink:href', href);
      }
    }
  }

  //動画が表示されるのを待つ
  function waitShowVideo() {
    log('waitShowVideo');
    setTimeout(function() {
      changeableVolume();
      if (returnVideo() && !isNaN(returnVideo().duration)) startObserve();
      else {
        clearInterval(interval.video);
        interval.video = setInterval(function() {
          changeableVolume();
          if (returnVideo() && !isNaN(returnVideo().duration)) {
            clearInterval(interval.video);
            flag.countWaitShowVideo = 0;
            startObserve();
          } else if (flag.countWaitShowVideo > 50) {
            clearInterval(interval.video);
            flag.countWaitShowVideo = 0;
          } else flag.countWaitShowVideo++;
        }, 200);
      }
    }, 400);
  }

  var sid = 'VolumeControl',
    ls = JSON.parse(localStorage.getItem(sid)) || {},
    observerC = new MutationObserver(checkChangeElements),
    observerT = new MutationObserver(changeProgramTitle),
    moConfig = { attributes: true, characterData: true },
    moConfig2 = { childList: true },
    moConfig3 = { childList: true, subtree: true },
    flag = { countWaitShowVideo: 0, mute: false, type: 0, wheel: false },
    interval = { chhannel: 0, info: 0, mute: 0, video: 0, wheel: 0 },
    observerS, eSlider;
  init();

})();