ABEMA Auto Adjust Playback Position

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

目前為 2022-09-27 提交的版本,檢視 最新版本

// ==UserScript==
// @name         ABEMA Auto Adjust Playback Position
// @namespace    https://gf.qytechs.cn/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);
})();

QingJ © 2025

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