Greasy Fork

Greasy Fork is available in English.

ABEMA Auto Adjust Playback Position

ABEMAで放送中の番組の遅延をなるべく改善します。

当前为 2022-10-29 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         ABEMA Auto Adjust Playback Position
// @namespace    http://greasyfork.icu/scripts/451815
// @version      4
// @description  ABEMAで放送中の番組の遅延をなるべく改善します。
// @match        https://abema.tv/*
// @grant        none
// @license      MIT License
// ==/UserScript==

// @ts-check
(() => {
  'use strict';

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

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

  // 倍速再生時の速度倍率
  // 初期値:1.5
  // 有効値:1.1 ~ 2.0
  let playbackRate = 0;

  // 生放送でのバッファの下限(秒数)
  // 初期値:3
  // 有効値:1 ~ 10
  let liveBuffer = 0;

  // 遅延を積極的に減らす(1:有効 / 2:無効)
  // 初期値:1
  // 有効値:1 ~ 2
  let activelyAdjust = 0;

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

  const sid = 'AutoAdjustPlaybackPosition',
    ls = JSON.parse(localStorage.getItem(sid) || '{}') || {},
    buffer = {
      archive: 15,
      changeableRate: true,
      cm: false,
      count: 0,
      currentMax: 0,
      currentMin: 0,
      /** @type {number[]} */
      max: [],
      /** @type {number[]} */
      min: [],
      originalArchive: 0,
      originalLive: 0,
      prev: 0,
      similarLive: false,
    },
    interval = { buffer: 0, init: 0, speed: 0, video: 0 },
    moConfig = { childList: true, subtree: true },
    selector = {
      footerText: '.com-tv-LinearFooter__feed-super-text',
      inner: '.c-application-DesktopAppContainer__content',
      liveIcon: '.com-a-LegacyIcon__red-icon-path[aria-label="生放送"]',
      main: 'main',
      splash: '.com-a-Video__video',
      video: 'video[src]:not([style*="display: none;"])',
    };

  /**
   * ページにイベントリスナーを追加
   */
  const addEventPage = () => {
    const id = document.querySelector(`.${sid}_Event`);
    if (!id) {
      log('addEventPage');
      const inner = document.querySelector(selector.inner);
      if (inner) {
        inner.classList.add(`${sid}_Event`);
      }
    }
  };

  /**
   * 動画の再生速度を変更する
   * @param {number} t 変更する時間(秒)
   * @param {number} r 速度の倍率
   */
  const changePlaybackSpeed = (t, r) => {
    clearInterval(interval.speed);
    const vi = returnVideo();
    if (t && r) {
      t = (t / r) * 2;
      log('Start change playback speed', t.toFixed(2), r);
      vi.playbackRate = r;
      interval.speed = setInterval(() => {
        clearInterval(interval.speed);
        log('Stop change playback speed', t.toFixed(2), r);
        vi.playbackRate = 1;
        resetBufferObj();
      }, t * 1000);
    } else if (vi.playbackRate !== 1) {
      log('Reset playback speed');
      vi.playbackRate = 1;
      resetBufferObj();
    }
  };

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

  /**
   * フッターに番組プログラムのテキストがあるか調べる
   * @returns {boolean}
   */
  const checkExistsFooterText = () => {
    const span = document.querySelector(selector.footerText);
    return span ? true : false;
  };

  /**
   * フッターに生放送アイコンがあるか調べる
   * @returns {boolean}
   */
  const cheeckExistsFooterLiveIcon = () => {
    const svg = document.querySelector(selector.liveIcon);
    return svg ? true : false;
  };

  /**
   * キーボードのキーを押したとき
   * @param {KeyboardEvent} e
   */
  const checkKeyDown = (e) => {
    const isInput =
      e.target instanceof HTMLInputElement ||
      e.target instanceof HTMLTextAreaElement
        ? true
        : false;
    if ((isInput && (e.altKey || e.ctrlKey)) || !isInput) {
      if (e.key === ',' || e.key === '[') {
        e.stopPropagation();
        changePlaybackSpeed(10, 0.5);
      } else if (e.key === '.' || e.key === ']') {
        e.stopPropagation();
        changePlaybackSpeed(30, 2);
      } else if (e.key === '/' || e.key === '\\') {
        e.stopPropagation();
        changePlaybackSpeed(0, 1);
      }
    }
  };

  /**
   * 動画のバッファを調べる
   */
  const checkVideoBuffer = () => {
    clearInterval(interval.buffer);
    interval.buffer = setInterval(() => {
      const vi = returnVideo();
      if (
        /^https:\/\/abema\.tv\/now-on-air\/[\w-]+\/?$/.test(location.href) &&
        vi?.buffered?.length
      ) {
        const live = cheeckExistsFooterLiveIcon(),
          after = live ? ' [LIVE]' : '',
          b = Math.floor((vi.buffered.end(0) - vi.currentTime) * 10) / 10,
          dur = vi.duration > 20000000000 ? true : false,
          len = vi.buffered.length,
          rate = vi.playbackRate,
          slow = 0.8;
        if (buffer.currentMax < b) buffer.currentMax = b;
        if (buffer.currentMin > b || buffer.currentMin === 0) {
          buffer.currentMin = b;
        }
        if (len > 1) {
          log('***** vi.buffered.length *****', len);
          for (let i = 0, l = len; i < l; i++) {
            log(i, vi.currentTime, vi.buffered.start(i), vi.buffered.end(i));
          }
        }
        if (b > 0 && buffer.changeableRate && dur && checkExistsFooterText()) {
          if (buffer.cm) {
            log('***** CM out *****');
            buffer.cm = false;
          }
          if (rate >= 1 && b < 1 && len === 1) {
            //現在のバッファが1秒未満になったときスロー再生する
            if (live) liveBuffer += 0.5;
            else buffer.archive += 0.5;
            const buff = live ? liveBuffer : buffer.archive;
            log('## A', rate, b, live, buff);
            changePlaybackSpeed(1.2 - b, slow);
          } else if (rate >= 1 && b < 2 && !live && len === 1) {
            //生放送以外で現在のバッファが2秒未満になったときスロー再生する
            buffer.archive += 0.5;
            log('## B', rate, b, live, buffer.archive);
            changePlaybackSpeed(3 - b, slow);
          } else if (rate > 1 && b < 8 && !live && !buffer.similarLive) {
            //生放送以外で倍速再生中に現在のバッファが8秒未満になったとき等速再生に戻す
            log('## C', rate, b, live);
            changePlaybackSpeed(0, 1);
          } else if (
            buffer.prev < b &&
            buffer.currentMax - buffer.currentMin > 1
          ) {
            buffer.max.push(buffer.currentMax);
            buffer.min.push(buffer.currentMin);
            buffer.currentMax = 0;
            buffer.currentMin = 0;
            buffer.count += 1;
            let time = 0;
            const maxLast = [...buffer.max].slice(-10),
              minLast = [...buffer.min].slice(-10),
              maxBottom = maxLast.reduce((x, y) => Math.min(x, y)),
              minBottom = minLast.reduce((x, y) => Math.min(x, y)),
              maxDiff =
                Math.round(
                  (maxLast.reduce((x, y) => Math.max(x, y)) - maxBottom) * 100
                ) / 100,
              minDiff =
                Math.round(
                  (minLast.reduce((x, y) => Math.max(x, y)) - minBottom) * 100
                ) / 100,
              lb1 = liveBuffer <= 3.5 ? 2 : liveBuffer <= 6.5 ? 4 : 6,
              lb2 = liveBuffer > 3 ? liveBuffer : 3;
            if (rate === 1) {
              if ((maxDiff >= 1 || minDiff >= 1) && live && len === 1) {
                //生放送時に最大/最小バッファのどちらかの差分が1秒以上のとき
                //最低バッファが4秒になるようスロー再生する
                time = Math.round((4 - minBottom) * 100) / 100;
                log('## D', time, b, maxDiff, minDiff, live);
                changePlaybackSpeed(time, slow);
              } else if (
                //最大バッファがbuffer.archiveより多いとき
                //最大バッファがbuffer.archiveに近づくよう倍速再生する
                maxLast.length >= 3 &&
                maxBottom > buffer.archive + 0.5
              ) {
                time = Math.round((maxBottom - buffer.archive) * 100) / 100;
                log('## E', time, b, maxDiff, minDiff, live);
                changePlaybackSpeed(time, playbackRate);
              } else if (
                //生放送&最小バッファがliveBufferより多い&バッファが安定し続けているとき
                //最小バッファがliveBufferに近づくよう倍速再生する
                live &&
                minLast.length >= 5 &&
                minBottom > liveBuffer + 0.5 &&
                maxDiff < 0.5 &&
                minDiff < 0.5
              ) {
                time = Math.round((minBottom - liveBuffer) * 100) / 100;
                log('## F', time, b, maxDiff, minDiff);
                changePlaybackSpeed(time, playbackRate);
              } else if (
                //生放送でバッファが安定しつづけているとき最小バッファを
                //liveBufferよりも減らすよう(下限は2秒)倍速再生する
                activelyAdjust === 1 &&
                live &&
                minLast.length >= 10 &&
                minBottom > lb1 + 0.5 &&
                maxDiff < 0.5 &&
                minDiff < 0.5
              ) {
                time = Math.round((minBottom - lb1) * 100) / 100;
                log('## G', time, b, maxDiff, minDiff);
                changePlaybackSpeed(time, playbackRate);
              } else if (
                //生放送以外で最小バッファが9秒に近づくよう倍速再生する
                activelyAdjust === 1 &&
                !live &&
                minLast.length >= 10 &&
                minBottom > 9.5 &&
                maxDiff < 0.5
              ) {
                time = Math.round((minBottom - 9) * 100) / 100;
                log('## H', time, b, maxDiff, minDiff);
                changePlaybackSpeed(time, playbackRate);
              } else if (
                //生放送以外でバッファが生放送のように安定し続けているとき
                //最小バッファがliveBuffer(下限は3秒)に近づくよう倍速再生する
                activelyAdjust === 1 &&
                !live &&
                minLast.length >= 10 &&
                minBottom > lb2 + 0.5 &&
                maxDiff < 0.5 &&
                minDiff < 0.5
              ) {
                buffer.similarLive = true;
                time = Math.round((minBottom - lb2) * 100) / 100;
                log('## I', time, b, maxDiff, minDiff);
                changePlaybackSpeed(time, playbackRate);
              }
            }
            log(
              buffer.count,
              'max:[',
              buffer.max.slice(-5).join('  '),
              ']',
              maxBottom,
              maxDiff,
              'min:[',
              buffer.min.slice(-5).join('  '),
              ']',
              minBottom,
              minDiff,
              live,
              len
            );
          }
        } else if (b <= 0 && checkExistsFooterText()) {
          log(
            '** -b',
            rate,
            b,
            buffer.currentMax,
            buffer.currentMin,
            live,
            len
          );
        } else if (!buffer.cm) {
          log('***** CM in *****', dur);
          buffer.cm = true;
          buffer.archive = buffer.originalArchive;
          buffer.count = 0;
          buffer.currentMax = 0;
          buffer.currentMin = 0;
          buffer.max = [];
          buffer.min = [];
          liveBuffer = buffer.originalLive;
          if (rate !== 1) {
            changePlaybackSpeed(0, 1);
          }
        }
        if (!buffer.changeableRate && !checkExistsFooterText()) {
          buffer.changeableRate = true;
          log('changeableRate', buffer.changeableRate);
        }
        buffer.prev = b;
        if (rate > 1) {
          showInfo(`▶▶ ×${rate}${after}`);
        } else if (rate > 0 && rate < 1) {
          showInfo(`▶ ×${rate}${after}`);
        }
      } else {
        clearInterval(interval.buffer);
        resetBufferObj();
      }
    }, 100);
  };

  /**
   * 情報を表示する要素をクリックしたとき
   */
  const clickInfo = () => {
    log('clickInfo');
    if (buffer.changeableRate) {
      changePlaybackSpeed(0, 1);
      buffer.changeableRate = false;
      log('changeableRate', buffer.changeableRate);
    }
  };

  /**
   * 情報を表示する要素を作成
   */
  const createInfo = () => {
    const css = `
      #${sid}_Info {
        align-items: center;
        background-color: rgba(0, 0, 0, 0.4);
        border-radius: 4px;
        bottom: 105px;
        color: #fff;
        display: flex;
        font-family: sans-serif;
        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: 2270;
      }
      #${sid}_Info.aapp_show {
        opacity: 0.8;
        visibility: visible;
      }
      #${sid}_Info:hover.aapp_show {
        background-color: rgba(0, 0, 0, 1);
        cursor: pointer;
        opacity: 1;
      }
      #${sid}_Info:hover.aapp_show:after {
        color: #cc9;
        content: "クリックで等速再生";
        padding-left: 1em;
      }
      #${sid}_Info.aapp_hidden {
        opacity: 0;
        transition: opacity 0.5s ease-out, visibility 0.5s ease-out;
        visibility: hidden;
      }
    `,
      div = document.createElement('div'),
      style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);
    div.id = `${sid}_Info`;
    div.innerHTML = '';
    div.addEventListener('click', clickInfo);
    document.body.appendChild(div);
  };

  /**
   * ページを開いたときに1度だけ実行
   */
  const init = () => {
    log('init');
    setupSettings();
    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);
      }
    }
  };

  /**
   * bufferオブジェクトをリセット
   */
  const resetBufferObj = () => {
    log('resetBufferObj');
    buffer.count = 0;
    buffer.currentMax = 0;
    buffer.currentMin = 0;
    buffer.max = [];
    buffer.min = [];
    buffer.prev = 0;
    buffer.similarLive = false;
  };

  /**
   * video要素を返す
   * @returns {*}
   */
  const returnVideo = () => {
    const vi = document.querySelector(selector.video);
    return vi ? vi : null;
  };

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

  /**
   * 設定の値を用意する
   */
  const setupSettings = () => {
    let rate = Number.isFinite(Number(playbackRate)) ? Number(playbackRate) : 0,
      buff = Number.isFinite(Number(liveBuffer)) ? Number(liveBuffer) : 0,
      act = Number.isFinite(Number(activelyAdjust))
        ? Number(activelyAdjust)
        : 0;
    rate = rate > 2 ? 2 : rate < 1.1 && rate !== 0 ? 1.1 : rate;
    buff = buff > 10 ? 10 : buff < 1 && buff !== 0 ? 1 : buff;
    act = act > 2 ? 2 : act < 1 && act !== 0 ? 1 : act;
    playbackRate = ls.playbackRate ? ls.playbackRate : rate ? rate : 1.5;
    liveBuffer = ls.liveBuffer ? ls.liveBuffer : buff ? buff : 3;
    activelyAdjust = ls.activelyAdjust ? ls.activelyAdjust : act ? act : 1;
    if (rate && ls.playbackRate !== rate) {
      playbackRate = rate;
      ls.playbackRate = rate;
      saveLocalStorage();
    }
    if (buff && ls.liveBuffer !== buff) {
      liveBuffer = buff;
      ls.liveBuffer = buff;
      saveLocalStorage();
    }
    if (act && ls.activelyAdjust !== act) {
      activelyAdjust = act;
      ls.activelyAdjust = act;
      saveLocalStorage();
    }
    buffer.originalArchive = buffer.archive;
    buffer.originalLive = liveBuffer;
  };

  /**
   * 情報を表示
   * @param {string} s 表示する文字列
   */
  const showInfo = (s) => {
    const eInfo = document.getElementById(`${sid}_Info`);
    if (eInfo) {
      eInfo.textContent = s ? s : '';
      eInfo.classList.remove('aapp_hidden');
      eInfo.classList.add('aapp_show');
      clearTimeout(interval.info);
      interval.info = setTimeout(() => {
        eInfo.classList.remove('aapp_show');
        eInfo.classList.add('aapp_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(selector.main);
    if (main) observerC.observe(main, moConfig);
    else log('startFirstObserve: Not found element.', 'error');
  };

  /**
   * 動画が表示されるのを待つ
   */
  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(() => {
      if (returnVideo() && !isNaN(returnVideo().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\/[\w-]+\/?$/.test(location.href)) {
      clearInterval(interval.init);
      init();
    }
  }, 1000);
})();