ABEMA Auto Adjust Playback Position

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

目前為 2022-10-15 提交的版本,檢視 最新版本

// ==UserScript==
// @name         ABEMA Auto Adjust Playback Position
// @namespace    https://gf.qytechs.cn/scripts/451815
// @version      3
// @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,
      count: 0,
      currentMax: 0,
      currentMin: 0,
      /** @type {number[]} */
      max: [],
      /** @type {number[]} */
      min: [],
      originalArchive: 0,
      originalLive: 0,
      prev: 0,
    },
    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 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 (vi.buffered.length > 1) {
          log('*** vi.buffered.length ***', vi.buffered.length);
          for (let i = 0, l = vi.buffered.length; i < l; i++) {
            log(i, vi.currentTime, vi.buffered.start(i), vi.buffered.end(i));
          }
        }
        if (
          b > 0 &&
          buffer.changeableRate &&
          vi.duration > 20000000000 &&
          checkExistsFooterText()
        ) {
          if (vi.playbackRate >= 1 && b < 1) {
            //現在のバッファが1秒未満になったときスロー再生する
            if (live) liveBuffer += 0.5;
            else buffer.archive += 0.5;
            const buff = live ? liveBuffer : buffer.archive;
            log('## A', vi.playbackRate, b, live, buff);
            changePlaybackSpeed(1.2 - b, slow);
          } else if (vi.playbackRate >= 1 && b < 2 && !live) {
            //生放送以外で現在のバッファが2秒未満になったときスロー再生する
            buffer.archive += 0.5;
            log('## B', vi.playbackRate, b, live, buffer.archive);
            changePlaybackSpeed(3 - b, slow);
          } else if (vi.playbackRate > 1 && b < 8 && !live) {
            //生放送以外で倍速再生中に現在のバッファが8秒未満になったとき等速再生に戻す
            log('## C', vi.playbackRate, b, 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;
            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 (vi.playbackRate === 1) {
              if ((maxDiff >= 1 || minDiff >= 1) && live) {
                //生放送時に最大/最小バッファのどちらかの差分が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
              ) {
                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,
              vi.buffered.length
            );
          }
        } else if (b <= 0 && checkExistsFooterText()) {
          log(
            '** -b',
            vi.playbackRate,
            b,
            buffer.currentMax,
            buffer.currentMin,
            live,
            vi.buffered.length
          );
        } else {
          buffer.archive = buffer.originalArchive;
          buffer.count = 0;
          buffer.currentMax = 0;
          buffer.currentMin = 0;
          buffer.max = [];
          buffer.min = [];
          liveBuffer = buffer.originalLive;
          if (vi.playbackRate !== 1) {
            changePlaybackSpeed(0, 0);
          }
        }
        if (!buffer.changeableRate && !checkExistsFooterText()) {
          buffer.changeableRate = true;
          log('changeableRate', buffer.changeableRate);
        }
        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');
    if (buffer.changeableRate) {
      changePlaybackSpeed(0, 0);
      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;
  };

  /**
   * 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();
    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);
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址