Greasy Fork

Greasy Fork is available in English.

ABEMA Auto Adjust Playback Position

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

当前为 2022-09-27 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 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      2
// @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;

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

  const sid = 'AutoAdjustPlaybackPosition',
    ls = JSON.parse(localStorage.getItem(sid) || '{}') || {},
    buffer = {
      archive: 15,
      changeRate: true,
      count: 0,
      currentMax: 0,
      currentMin: 0,
      /** @type {number[]} */
      max: [],
      /** @type {number[]} */
      min: [],
      prev: 0,
    },
    interval = { buffer: 0, changeRate: 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 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 b = Math.floor((vi.buffered.end(0) - vi.currentTime) * 10) / 10,
          live = cheeckExistsFooterLiveIcon(),
          after = live ? ' [LIVE]' : '',
          slow = 0.8;
        if (buffer.currentMax < b) buffer.currentMax = b;
        if (buffer.currentMin > b || buffer.currentMin === 0) {
          buffer.currentMin = b;
        }
        if (
          buffer.changeRate &&
          vi.duration > 20000000000 &&
          checkExistsFooterText()
        ) {
          if (vi.playbackRate >= 1 && b < 1) {
            //現在のバッファが1秒未満になったときスロー再生する
            log(vi.playbackRate, b, buffer.currentMax, buffer.currentMin, live);
            changePlaybackSpeed(1.2 - b, slow);
          } else if (vi.playbackRate >= 1 && b < 2 && !live) {
            //生放送以外で現在のバッファが2秒未満になったときスロー再生する
            log(vi.playbackRate, b, buffer.currentMax, buffer.currentMin, live);
            changePlaybackSpeed(3 - b, slow);
          } else if (vi.playbackRate > 1 && b < 8 && !live) {
            //生放送以外で倍速再生中に現在のバッファが8秒未満になったとき等速再生に戻す
            log(vi.playbackRate, b, buffer.currentMax, buffer.currentMin, live);
            changePlaybackSpeed(0, 0);
          } 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;
            const maxLast5 = [...buffer.max].slice(-5),
              minLast5 = [...buffer.min].slice(-5),
              maxBottom = maxLast5.reduce((x, y) => Math.min(x, y)),
              minBottom = minLast5.reduce((x, y) => Math.min(x, y)),
              maxDiff =
                Math.round(
                  (maxLast5.reduce((x, y) => Math.max(x, y)) - maxBottom) * 100
                ) / 100,
              minDiff =
                Math.round(
                  (minLast5.reduce((x, y) => Math.max(x, y)) - minBottom) * 100
                ) / 100;
            if (vi.playbackRate === 1) {
              if (maxLast5.length >= 3 && maxBottom > buffer.archive + 1) {
                //最大バッファがbuffer.archiveより多いとき
                //最大バッファがbuffer.archiveに近づくよう倍速再生する
                log(
                  '--- changePlaybackSpeed  ---',
                  Math.round((maxBottom - buffer.archive) * 100) / 100,
                  b,
                  maxDiff,
                  minDiff,
                  live
                );
                changePlaybackSpeed(maxBottom - buffer.archive, playbackRate);
              } else if (
                live &&
                minLast5.length === 5 &&
                minBottom > liveBuffer + 1 &&
                maxDiff < 0.5 &&
                minDiff < 0.5
              ) {
                //生放送&最小バッファがliveBufferより多い&バッファが安定し続けているとき
                //最小バッファがliveBufferに近づくよう倍速再生する
                log(
                  '--- changePlaybackSpeed LIVE ---',
                  Math.round((minBottom - liveBuffer) * 100) / 100,
                  b,
                  maxDiff,
                  minDiff
                );
                changePlaybackSpeed(minBottom - liveBuffer, playbackRate);
              }
            }
            log(
              buffer.count,
              ' max: [',
              buffer.max.slice(-5).join(' '),
              '] ',
              maxDiff,
              ' min: [',
              buffer.min.slice(-5).join(' '),
              '] ',
              minDiff,
              live,
              vi.buffered.length
            );
          }
        } else {
          buffer.count = 0;
          buffer.currentMax = 0;
          buffer.currentMin = 0;
          buffer.max = [];
          buffer.min = [];
          changePlaybackSpeed(0, 0);
        }
        buffer.prev = b;
        if (vi.playbackRate > 1) {
          showInfo(`▶▶ ×${vi.playbackRate}${after}`);
        } else if (vi.playbackRate < 1) {
          showInfo(`▶ ×${vi.playbackRate}${after}`);
        }
      } else {
        clearInterval(interval.buffer);
        resetBufferObj();
      }
    }, 100);
  };

  /**
   * 動画を構成している要素に変更があったとき
   */
  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;
  };

  /**
   * 情報を表示する要素をクリックしたとき
   */
  const clickInfo = () => {
    log('clickInfo start');
    changePlaybackSpeed(0, 0);
    buffer.changeRate = false;
    clearInterval(interval.changeRate);
    interval.changeRate = setTimeout(() => {
      log('clickInfo end');
      buffer.changeRate = true;
    }, 90000);
  };

  /**
   * 情報を表示する要素を作成
   */
  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.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) {
      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());
    }
  };

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

  /**
   * 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;
    rate = rate > 2 ? 2 : rate < 1.1 && rate !== 0 ? 1.1 : rate;
    buff = buff > 10 ? 10 : buff < 1 && buff !== 0 ? 1 : buff;
    playbackRate = ls.playbackRate ? ls.playbackRate : rate ? rate : 1.5;
    liveBuffer = ls.liveBuffer ? ls.liveBuffer : buff ? buff : 3;
    if (rate && ls.playbackRate !== rate) {
      playbackRate = rate;
      ls.playbackRate = rate;
      saveLocalStorage();
    }
    if (buff && ls.liveBuffer !== buff) {
      liveBuffer = buff;
      ls.liveBuffer = buff;
      saveLocalStorage();
    }
  };

  /**
   * 情報を表示
   * @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();
    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);
})();