Greasy Fork

Greasy Fork is available in English.

ABEMA Auto Adjust Playback Position

ABEMAで視聴している番組の遅延(タイムラグ)を減らします。

当前为 2022-11-30 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 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      8
// @description  ABEMAで視聴している番組の遅延(タイムラグ)を減らします。
// @match        https://abema.tv/*
// @grant        none
// @license      MIT License
// ==/UserScript==

(() => {
  '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;

  // フッターに動画の解像度と表示サイズを表示する(1:表示 / 2:非表示)
  // 初期値:1
  // 有効値:1 ~ 2
  let resolution = 0;

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

  const sid = 'AutoAdjustPlaybackPosition',
    ls = JSON.parse(localStorage.getItem(sid) || '{}') || {},
    buffer = {
      archive: 15,
      changeableRate: true,
      cm: false,
      count: 0,
      currentMax: 0,
      currentMin: 0,
      currentTime: 0,
      large: false,
      /** @type {number[]} */
      max: [],
      /** @type {number[]} */
      min: [],
      originalArchive: 0,
      originalLive: 0,
      prev: 0,
      similarLive: false,
    },
    interval = {
      buffer: 0,
      footer: 0,
      init: 0,
      resolution: 0,
      sidepanel: 0,
      speed: 0,
      splash: 0,
      video: 0,
    },
    moConfig = { childList: true, subtree: true },
    selector = {
      footer: '.com-tv-LinearFooter,.com-vod-PayperviewLinearControlBar,.com-live-event-LiveEventVideoController',
      footerText:
        '.com-tv-LinearFooter__feed-super-text,.com-live-event-LiveEventTitle',
      inner: '.c-application-DesktopAppContainer__content',
      liveIcon:
        '.com-a-LegacyIcon__red-icon-path[aria-label="生放送"],.com-live-event-LiveEventViewCounter__icon-wrapper',
      main: 'main',
      resolution:
        '.com-tv-TVScreen__footer-container,.com-live-event-LiveEventPlayerAreaLayout',
      visibleFooter:
        '.com-tv-TVScreen__footer-container:not(.com-tv-TVScreen__footer-container--hidden),.com-live-event-LiveEventPlayerAreaLayout--controllers-visible',
      splash: '.com-a-Video__video,.com-live-event__LiveEventPlayerView',
      video: 'video[src]:not([style*="display: none;"])',
    },
    video = {
      clientHeight: 0,
      clientWidth: 0,
      pixelRatio: 0,
      src: '',
      videoHeight: 0,
      videoWidth: 0,
    };

  /**
   * スタイルシートを追加
   */
  const addCSS = () => {
    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;
      }
      #${sid}_video-resolution {
        bottom: 2px;
        color: #ccc;
        font-size: 12px;
        height: 14px;
        left: 157px;
        position: absolute;
      }
    `,
      style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);
  };

  /**
   * 必要ならオブザーバーを追加する
   */
  const addObserver = () => {
    if (resolution === 1) {
      const re = document.querySelector(`.${sid}_Resolution`);
      if (!re) {
        const reso = document.querySelector(selector.resolution);
        if (reso) {
          reso.classList.add(`${sid}_Resolution`);
          observerF.observe(reso, { attributes: true });
        }
      }
    }
  };

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

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

  /**
   * 動画のソースが切り替わったとき
   */
  const checkChangeVideoSource = () => {
    const vi = returnVideo();
    if (vi?.hasAttribute('src')) {
      const src = vi.getAttribute('src');
      if (src && src !== video.src) {
        log('checkChangeVideoSource');
        video.src = src;
        checkResolution();
      }
    }
  };

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

  /**
   * 生放送を示すアイコンがあるか調べる
   * @returns {boolean}
   */
  const cheeckExistsLiveIcon = () => {
    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 checkResolution = () => {
    if (resolution === 1) {
      if (interval.resolution) clearTimeout(interval.resolution);
      interval.resolution = setTimeout(() => {
        const dpr = window.devicePixelRatio,
          footer = document.querySelector(selector.footer),
          vi = returnVideo(),
          vr = document.getElementById(`${sid}_video-resolution`),
          ch = vi?.clientHeight,
          cw = vi?.clientWidth,
          vh = vi?.videoHeight,
          vw = vi?.videoWidth;
        if (vi && dpr && ch && cw && vh && vw) {
          if (
            video.pixelRatio !== dpr ||
            video.clientHeight !== ch ||
            video.clientWidth !== cw ||
            video.videoHeight !== vh ||
            video.videoWidth !== vw
          ) {
            log('checkResolution');
            let desc = `動画解像度: ${vw}×${vh} / 表示サイズ: ${cw}×${ch}`;
            if (dpr !== 1) desc += ` * ${dpr}`;
            if (vr) {
              vr.innerHTML = `<span>${desc}</span>`;
            } else {
              const div = document.createElement('div');
              div.id = `${sid}_video-resolution`;
              div.innerHTML = `<span>${desc}</span>`;
              if (footer) footer.appendChild(div);
            }
            video.pixelRatio = dpr;
            video.clientHeight = ch;
            video.clientWidth = cw;
            video.videoHeight = vh;
            video.videoWidth = vw;
          }
        }
      }, 100);
    }
  };

  /**
   * 動画のバッファを調べる
   */
  const checkVideoBuffer = () => {
    clearInterval(interval.buffer);
    interval.buffer = setInterval(() => {
      const vi = returnVideo(),
        tv = /^https:\/\/abema\.tv\/now-on-air\/[\w-]+\/?$/.test(location.href),
        le = /^https:\/\/abema\.tv\/live-event\/[\w-]+\/?$/.test(location.href);
      if (
        (tv && vi?.buffered?.length) ||
        (le && vi?.buffered?.length && cheeckExistsLiveIcon())
      ) {
        const live = cheeckExistsLiveIcon(),
          after = live ? ' [LIVE]' : '',
          cTime = vi.currentTime,
          b = Math.floor((vi.buffered.end(0) - cTime) * 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 (buffer.large) {
            if (b < liveBuffer + 2 || (!live && b < buffer.archive - 3)) {
              log('small buffer', buffer.currentMax, b);
              changePlaybackSpeed(0, 1);
            }
          }
        }
        if (buffer.currentTime > cTime && !buffer.cm && tv) {
          const ct = buffer.currentTime - cTime;
          vi.currentTime += ct + 0.2;
          log(
            `${ct.toFixed(2)}秒巻き戻ったので元の位置へシークしました`,
            b,
            buffer.currentMin,
            len,
            'warn'
          );
        }
        if (b > 0 && buffer.changeableRate && dur && checkExistsFooterText()) {
          if (buffer.cm) {
            log('***** CM out *****');
            buffer.cm = false;
          }
          if (len > 1) {
            log('***** vi.buffered.length *****', len);
            for (let i = 0, l = len; i < l; i++) {
              log(i, cTime, vi.buffered.start(i), vi.buffered.end(i));
            }
          }
          if (rate >= 1 && b < 1 && len === 1 && tv) {
            //現在のバッファが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;
            if (
              //記録したバッファが参考にならない値だと判断した場合は破棄する
              (buffer.max.length === 2 &&
                buffer.min.length === 2 &&
                (buffer.max[0] + 5 < buffer.max[1] ||
                  buffer.min[0] + 5 < buffer.min[1])) ||
              (buffer.max.length > 1 &&
                buffer.min.length > 1 &&
                (buffer.max.slice(-1)[0] < 0 || buffer.min.slice(-1)[0] < 0))
            ) {
              log('** shift', buffer.max.slice(-1)[0], buffer.min.slice(-1)[0]);
              buffer.max.shift();
              buffer.min.shift();
            } else 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 (
                //最大バッファがbuffer.archiveより多いとき
                //最大バッファがbuffer.archiveに近づくよう倍速再生する
                maxLast.length >= 3 &&
                maxBottom > buffer.archive + 0.5
              ) {
                time = Math.round((maxBottom - buffer.archive) * 100) / 100;
                if (minBottom > 19) {
                  buffer.large = true;
                  time = 999;
                }
                log('## E', time, b, maxDiff, minDiff, live, minBottom);
                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.currentTime = 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.currentTime = cTime;
        buffer.prev = b;
        if (rate > 1) {
          showInfo(`▶▶ ×${rate}${after}`);
        } else if (rate > 0 && rate < 1) {
          showInfo(`▶ ×${rate}${after}`);
        }
      } else resetBufferObj();
    }, 100);
  };

  /**
   * VIDEO要素があるか調べる
   */
  const checkVideoElement = () => {
    clearInterval(interval.video);
    interval.video = setInterval(() => {
      const vi = returnVideo();
      if (vi) {
        clearInterval(interval.video);
        if (!vi.classList.contains(`${sid}_VideoElement`)) {
          log('checkVideoElement');
          vi.classList.add(`${sid}_VideoElement`);
          if (resolution === 1) observerR.observe(vi);
          observerV.observe(vi, { attributes: true });
        }
      }
    }, 500);
  };

  /**
   * 動画のフッターが表示されているかを調べる
   */
  const checkVisibleFooter = () => {
    const cvf = () => {
      const fo = document.querySelector(selector.visibleFooter);
      if (fo) checkResolution();
      else clearInterval(interval.footer);
    };
    clearInterval(interval.footer);
    interval.footer = setInterval(cvf, 500);
    cvf();
  };

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

  /**
   * 情報を表示する要素を作成
   */
  const createInfo = () => {
    const div = document.createElement('div');
    div.id = `${sid}_Info`;
    div.innerHTML = '';
    div.addEventListener('click', clickInfo);
    document.body.appendChild(div);
  };

  /**
   * ページを開いたときに1度だけ実行
   */
  const init = () => {
    log('init');
    setupSettings();
    waitShowVideo();
    addCSS();
    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 = () => {
    buffer.count = 0;
    buffer.currentMax = 0;
    buffer.currentMin = 0;
    buffer.currentTime = 0;
    buffer.large = false;
    buffer.max = [];
    buffer.min = [];
    buffer.prev = 0;
    buffer.similarLive = false;
  };

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

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

  /**
   * 設定の値を用意する
   */
  const setupSettings = () => {
    /**
     * Settings欄で設定した変数の値が数字以外なら0にする
     * @param {number} a Settings欄の変数
     * @returns {number}
     */
    const num = (a) => (Number.isFinite(Number(a)) ? Number(a) : 0);
    let rate = num(playbackRate),
      buff = num(liveBuffer),
      act = num(activelyAdjust),
      res = num(resolution);
    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;
    res = res > 2 ? 2 : res < 1 && res !== 0 ? 1 : res;
    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;
    resolution = ls.resolution ? ls.resolution : res ? res : 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();
    }
    if (res && ls.resolution !== act) {
      resolution = res;
      ls.resolution = res;
      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');
    addObserver();
    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.splash);
    interval.splash = setInterval(() => {
      const vi = returnVideo();
      if (vi && !isNaN(vi.duration) && splash()) {
        clearInterval(interval.splash);
        startFirstObserve();
      }
    }, 250);
  };

  const observerC = new MutationObserver(checkChangeElements),
    observerF = new MutationObserver(checkVisibleFooter),
    observerR = new ResizeObserver(checkResolution),
    observerV = new MutationObserver(checkChangeVideoSource);
  clearInterval(interval.init);
  interval.init = setInterval(() => {
    if (
      /^https:\/\/abema\.tv\/(now-on-air|live-event)\/[\w-]+\/?$/.test(
        location.href
      )
    ) {
      clearInterval(interval.init);
      init();
    }
  }, 1000);
})();