Greasy Fork

Greasy Fork is available in English.

AbemaTV Auto Reload

AbemaTV(HTML5版)閲覧中に動画が止まったとき、動画を再読み込みします。

目前为 2018-01-20 提交的版本,查看 最新版本

// ==UserScript==
// @name         AbemaTV Auto Reload
// @namespace    http://greasyfork.icu/ja/scripts/25598
// @version      14
// @description  AbemaTV(HTML5版)閲覧中に動画が止まったとき、動画を再読み込みします。
// @include      https://abema.tv/
// @include      https://abema.tv/now-on-air/*
// @grant        none
// ==/UserScript==

(() => {
  'use strict';

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

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

  // 画質を変更したとき、左上に情報を表示するかどうか
  // 1:表示する / 2:表示しない
  // 初期値:1
  // 有効値:1 ~ 2
  let displayInformation = 0;

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


  const sid = 'AutoReload',
    ls = JSON.parse(localStorage.getItem(sid)) || {},
    ss = JSON.parse(sessionStorage.getItem(sid)) || {},
    moConfig = { attributes: true, characterData: true },
    moConfig2 = { childList: true },
    flag = {
      bufferedEnd: 0,
      change1: false,
      change2: false,
      changeChannel: false,
      checkKeyUp: false,
      checkQuality: false,
      countWaitShowVideo: 0,
      currentTime: 0,
      loadstart: false,
      reload: false,
      show: false,
      type: 0,
      undoChannel: false
    },
    timer = {
      init: 0,
      loading: 0,
      notify: 0,
      playerActiveQualityChanged: 0,
      reload: 0,
      showInfo: 0,
      showQuality: 0,
      showSpec: 0,
      waitShowVideo: 0
    },
    video = {
      height: [-1, 180, 240, 360, 480, 720, 1080],
      quality: ['自動で最適化', '通信節約モード', '最低画質', '低画質', '中画質', '高画質', '最高画質']
    };
  let eV, ePrev, eNext, chId, programTitle, programStartTime;

  //ページにイベントリスナーを追加
  const addEventPage = () => {
    log('addEventPage');
    document.addEventListener('keyup', checkKeyUp, false);
  };

  //動画にイベントリスナーを追加
  const addEventVideo = s => {
    log('addEventVideo', s);
    eV = returnVideo(`addEventVideo2 ${s}`);
    if (!eV) {
      checkChangeVideo('addEventVideo2');
      return;
    }
    if (!eV.classList.contains(sid)) {
      eV.classList.add(sid);
      eV.addEventListener('error', videoError, false);
      eV.addEventListener('playing', videoPlaying, false);
      eV.addEventListener('stalled', videoStalled, false);
      eV.addEventListener('waiting', videoWaiting, false);
      if (flag.type === 1) {
        if (theoplayer.player && theoplayer.player(0).videoTracks.item(0).qualities) theoplayer.player(0).videoTracks.item(0).qualities.addEventListener('activequalitychanged', playerActiveQualityChanged, false);
      }
      if (ls.debug) {
        eV.addEventListener('canplay', videoCanplay, false);
        eV.addEventListener('canplaythrough', videoCanplaythrough, false);
        eV.addEventListener('durationchange', videoDurationchange, false);
        eV.addEventListener('emptied', videoEmpied, false);
        eV.addEventListener('ended', videoEnded, false);
        eV.addEventListener('loadeddata', videoLoadeddata, false);
        eV.addEventListener('loadedmetadata', videoLoadedmetadata, false);
        eV.addEventListener('loadstart', videoLoadstart, false);
        eV.addEventListener('pause', videoPause, false);
        eV.addEventListener('play', videoPlay, false);
        eV.addEventListener('progress', videoProgress, false);
        eV.addEventListener('seeked', videoSeeked, false);
        eV.addEventListener('seeking', videoSeeking, false);
        eV.addEventListener('suspend', videoSuspend, false);
        eV.addEventListener('timeupdate', videoTimeupdate, false);
      }
      checkQuality('addEventVideo');
    }
  };

  //チャンネルを切り替える
  const changeChannel = s => {
    log('changeChannel', s);
    if (!flag.changeChannel && !flag.undoChannel) {
      flag.changeChannel = true;
      eNext.click();
      showInfo('チャンネルを切り替えます', 4000);
    }
  };

  //番組名が変更したとき
  const changeProgramTitle = () => {
    log('changeProgramTitle1');
    const title = returnFooterText(),
      time = returnProgramTime();
    if (title) {
      programTitle = title.textContent.trim() || '';
      log('returnFooterText2', programTitle);
      addEventVideo();
    }
    if (time) {
      const t = time.textContent || '';
      if (t) {
        programStartTime = t.slice(t.indexOf(')') + 1, t.indexOf(' '));
        log('returnFooterText3', programStartTime);
      }
    }
    checkPlayingVideo('changeProgramTitle');
  };

  //画質を変更する
  const changeQuality = (s, n, c, a) => {
    log('changeQuality', s, n, c, a, ss.tempQuality, flag.checkKeyUp);
    if (flag.type === 1) {
      const p = theoplayer.player(0);
      if (!flag.checkKeyUp && p && p.videoTracks && p.videoTracks.item) {
        const q = p.videoTracks.item(0).qualities;
        if (n === 48) {
          for (let i = 0, j = q.length; i < j; i++) {
            if (q.getQualityByID(i).hasOwnProperty('enabled')) {
              if (c && a) {
                for (let k = 0; k < j; k++) {
                  if (q.getQualityByID(k).hasOwnProperty('enabled')) q.getQualityByID(k).enabled = true;
                  ls.automaticQualities[k] = true;
                }
              } else if (ls.automaticQualities.length > i && q.getQualityByID(i).hasOwnProperty('enabled')) q.getQualityByID(i).enabled = ls.automaticQualities[i];
            }
          }
          if (!(c && a)) {
            q.targetQuality = null;
            ls.videoQuality = 0;
            ss.tempQuality = -1;
          }
        } else if (n >= 49 && n <= 54) {
          let id = n - 49,
            count = 0;
          if (c && a) {
            if (q.length >= id && !ls.videoQuality && ss.tempQuality === -1) {
              for (let i = 0, j = q.length; i < j; i++) {
                if (q.getQualityByID(i).enabled) count += 1;
              }
              if (count === 2 && q.getQualityByID(id).enabled) return;
              if (q.getQualityByID(id).hasOwnProperty('enabled')) {
                q.getQualityByID(id).enabled = !q.getQualityByID(id).enabled;
                ls.automaticQualities[id] = !ls.automaticQualities[id];
                if (displayInformation === 1) showInfo(`自動最適化での${video.quality[id + 1]}:${ls.automaticQualities[id] ? '有効' : '無効'}`);
              }
            }
          } else if (c) {
            if (id > q.length - 1) id = q.length - 1;
            if (q.getQualityByID(id).hasOwnProperty('enabled')) {
              q.getQualityByID(id).enabled = true;
              q.targetQuality = q.getQualityByID(id);
              ss.tempQuality = n - 48;
            }
          } else if (flag.checkKeyUp && ss.tempQuality >= 0) {
            if (q.getQualityByID(ss.tempQuality).hasOwnProperty('enabled')) {
              q.getQualityByID(ss.tempQuality).enabled = true;
              q.targetQuality = q.getQualityByID(ss.tempQuality);
            }
          } else {
            if (id > q.length - 1) id = q.length - 1;
            if (q.getQualityByID(id).hasOwnProperty('enabled')) {
              q.getQualityByID(id).enabled = true;
              q.targetQuality = q.getQualityByID(id);
              ls.videoQuality = n - 48;
              ss.tempQuality = -1;
            }
          }
        } else {
          q.targetQuality = null;
          ls.videoQuality = 0;
          ss.tempQuality = -1;
        }
        saveLocalStorage();
        saveSessionStorage();
      }
    }
  };

  //動画が切り替わったかを調べる
  const checkChangeVideo = async a => {
    log('checkChangeVideo');
    const e = returnVideo(`checkChangeVideo ${a}`);
    if (!flag.change1 && !e) {
      log('checkChangeVideo1', a);
      flag.change1 = true;
      await sleep(550);
      flag.change1 = false;
      checkChangeVideo('checkChangeVideo');
    } else if (!flag.change2 && e && (!eV || e.id !== eV.id)) {
      log('checkChangeVideo2', a);
      flag.change2 = true;
      await sleep(1000);
      flag.change2 = false;
      addEventVideo('checkChangeVideo');
    } else {
      log('checkChangeVideo3', a);
      checkPlayingVideo('checkChangeVideo3');
    }
  };

  //動画配信の形式を調べる
  const checkVideoFormat = () => {
    if (theoplayer && theoplayer.player && theoplayer.player(0)) {
      flag.type = 1;
      return true;
    }
    const vi = document.getElementsByTagName('video'),
      au = document.getElementsByTagName('audio');
    if (vi.length > 0 && vi.length === au.length && !document.getElementsByClassName('vjs-tech').length) {
      flag.type = 2;
      return true;
    }
    flag.type = 0;
    return false;
  };

  //キーボードのキーを押したとき
  const checkKeyUp = e => {
    if (/input|textarea/i.test(e.target.tagName)) return;
    if (e.shiftKey) {
      const quality = async () => {
          flag.checkKeyUp = true;
          await sleep(250);
          flag.checkKeyUp = false;
        },
        vi = returnVideo('checkKeyUp'),
        au = returnAudio();
      checkVideoFormat();
      if (e.keyCode === 8) { //Backspace
        if (vi) vi.playbackRate = 1;
        if (flag.type === 2 && au) au.playbackRate = 1;
        showInfo(`${vi.playbackRate}倍速`);
      } else if (e.keyCode === 32) { //Space
        if (flag.type === 1) {
          if (theoplayer.player(0).paused) theoplayer.player(0).play();
          else theoplayer.player(0).pause();
        } else if (flag.type === 2 && vi && au) {
          if (vi.paused) {
            vi.play();
            au.play();
          } else {
            vi.pause();
            au.pause();
          }
        }
      } else if (e.keyCode >= 48 && e.keyCode <= 54) { //0~6
        changeQuality('checkKeyUp', e.keyCode, e.ctrlKey, e.altKey);
        quality();
      } else if (e.keyCode === 57) checkQuality(); //9
      else if (e.keyCode === 73) showSpec(); //i
      else if (e.keyCode === 221 || e.keyCode === 219) { //]・[
        if (vi && vi.playbackRate) {
          const pr = vi.playbackRate.toFixed(1);
          if (e.keyCode === 221) {
            if (e.ctrlKey) {
              vi.playbackRate = 2;
              if (flag.type === 2 && au) au.playbackRate = 2;
            } else if (pr < 2) {
              vi.playbackRate += 0.1;
              if (flag.type === 2 && au) au.playbackRate += 0.1;
            }
          } else if (e.keyCode === 219) {
            if (e.ctrlKey) {
              vi.playbackRate = 0.5;
              if (flag.type === 2 && au) au.playbackRate = 0.5;
            } else if (pr > 0.5) {
              vi.playbackRate -= 0.1;
              if (flag.type === 2 && au) au.playbackRate -= 0.1;
            }
          }
          showInfo(`${Number(vi.playbackRate.toFixed(3))}倍速`, vi.playbackRate !== 1 ? -1 : null);
        }
      }
    }
  };

  //動画が実際に再生中なのかを調べる
  const checkPlayingVideo = async s => {
    log('checkPlayingVideo', s);
    eV = returnVideo('checkPlayingVideo1');
    if (eV) {
      let time1 = 0,
        time2 = 0;
      try { time1 = eV.currentTime; } catch(err) { log('checkPlayingVideo: time1', err, 'error'); }
      if (time1) {
        await sleep(250);
        try { time2 = eV.currentTime; } catch(err) { log('checkPlayingVideo: time2', err, 'error'); }
        if (time1 === time2) reloadVideo(`checkPlayingVideo1-${s}`);
      } else reloadVideo(`checkPlayingVideo2-${s}`);
    } else log('checkPlayingVideo: not found video');
  };

  //動画の画質を調べる
  const checkQuality = s => {
    log('checkQuality', s);
    if (flag.type === 1) {
      const p = theoplayer.player(0);
      if (p && p.videoTracks && p.videoTracks.item) {
        const q = p.videoTracks.item(0).qualities,
          aq = q.activeQuality,
          max = q.getQualityByID(q.length - 1);
        if (s) {
          flag.checkQuality = true;
          if (ss.tempQuality >= 0) changeQuality('checkQuality1', ss.tempQuality + 48);
          else if (ls.videoQuality >= 0) changeQuality('checkQuality2', ls.videoQuality + 48);
        } else if (aq.resolution.height) {
          const qn = ss.tempQuality >= 0 ? `*${ss.tempQuality}` : ls.videoQuality;
          let mes = `解像度:${p.videoWidth}×${p.videoHeight}\n` +
            `選択(${qn}):${aq.resolution.width}×${aq.resolution.height}, ${aq.frameRate}fps, ${(aq.bandwidth / 1000)}kbps`;
          if (max.resolution.height) mes += `\n最高(${(q.length)}):${max.resolution.width}×${max.resolution.height}, ${max.frameRate}fps, ${(max.bandwidth / 1000)}kbps`;
          notify('checkQuality', mes);
        }
      }
    } else if (flag.type === 2) {
      const vi = returnVideo(`checkQuality ${s}`);
      if (vi && vi.videoHeight) notify('checkQuality', `解像度:${vi.videoWidth}×${vi.videoHeight}`);
    }
  };

  //解像度や画質などの情報を表示する要素を作成
  const createInfo = () => {
    const css = '#AutoReload_Info, #AutoReload_Quality, #AutoReload_Spec {align-items:center; background-color:rgba(0,0,0,0.6); border-radius:4px; color:white; display:flex; justify-content:center; min-height:2em; min-width:4em; padding:0.5ex 1ex; position:fixed; -ms-user-select:none; -moz-user-select:none; -webkit-user-select:none; user-select:none;}' +
      '#AutoReload_Info {left:20px; top:80px; z-index: 2222; }' +
      '#AutoReload_Quality {left:20px; top:50px; z-index: 2224; }' +
      '#AutoReload_Spec {bottom:120px; font-size:90%; left:40px; width:calc(100% - 80px); max-width:60em; z-index: 2220; }' +
      '#AutoReload_Spec dl {display:flex; flex-wrap:wrap;}' +
      '#AutoReload_Spec dt {width:10em;}' +
      '#AutoReload_Spec dd {width:calc(100% - 10em); padding-left:1em;}' +
      '.ar_show {opacity:0.8; visibility:visible;}' +
      '.ar_hidden {opacity:0; visibility:hidden; transition:opacity 0.5s ease-out, visibility 0.5s ease-out;}',
      div1 = document.createElement('div'),
      div2 = document.createElement('div'),
      div3 = document.createElement('div'),
      style = document.createElement('style');
    style.type = 'text/css';
    style.textContent = css;
    document.head.appendChild(style);
    div1.id = 'AutoReload_Info';
    div1.className = 'ar_hidden';
    document.body.appendChild(div1);
    div2.id = 'AutoReload_Quality';
    div2.className = 'ar_hidden';
    document.body.appendChild(div2);
    div3.id = 'AutoReload_Spec';
    div3.className = 'ar_hidden';
    document.body.appendChild(div3);
  };

  //ページを開いたときに1度だけ実行
  const init = () => {
    log('init');
    setupSettings();
    waitShowVideo('init');
    createInfo();
  };

  //デバッグ用 ログ
  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());
      } else console.log(sid, a.toString());
    }
  };

  //デスクトップ通知
  const notify = (f, m, s) => {
    const title = 'AbemaTV Auto Reload',
      message = `${m}\n${new Date().toLocaleTimeString()}`,
      delay = s >= 1000 ? s : 3000;
    let notifi;
    log('----- notify -----', f, m);
    if ('Notification' in window) {
      if (Notification.permission === 'granted') {
        notifi = new Notification(title, { body: message, tag: f });
      } else if (Notification.permission !== 'denied') {
        Notification.requestPermission(permission => {
          if (permission === 'granted') {
            notifi = new Notification(title, { body: message, tag: f });
          }
        });
      }
      if (notifi) {
        clearTimeout(timer.notify);
        timer.notify = setTimeout(() => notifi.close(), delay);
      }
    } else console.log(title, message);
  };

  //画質が変更されたとき
  const playerActiveQualityChanged = () => {
    log('playerActiveQualityChanged');
    if (flag.type === 1) {
      const eQua = document.getElementById('AutoReload_Quality'),
        eSel = document.getElementsByClassName('theoplayer-configuration-panel-content')[0],
        p = theoplayer.player(0);
      let n = 0;
      if (!p || !p.videoTracks.item) return;
      if (ss.tempQuality > 0 && p.videoTracks.item(0).qualities.activeQuality.resolution.height !== video.height[ss.tempQuality]) changeQuality('playerActiveQualityChanged1', ss.tempQuality + 48);
      else if (ls.videoQuality > 0 && p.videoTracks.item(0).qualities.activeQuality.resolution.height !== video.height[ls.videoQuality]) changeQuality('playerActiveQualityChanged2', ls.videoQuality + 48);
      log(`${ls.videoQuality} / ${ss.tempQuality} : ${p.videoHeight} → ${p.videoTracks.item(0).qualities.activeQuality.resolution.height}`);
      if (displayInformation === 1) {
        clearInterval(timer.playerActiveQualityChanged);
        timer.playerActiveQualityChanged = setInterval(() => {
          if (p && p.videoTracks && p.videoTracks.item) {
            const w = p.videoWidth,
              h = p.videoHeight,
              a = p.videoTracks.item(0).qualities.activeQuality,
              r = (a) ? a.resolution : null;
            let after = '',
              before = '';
            log('activequalitychanged 1', w, h);
            if (r && r.width && r.height && isFinite(r.width) && isFinite(r.height)) {
              log('activequalitychanged 2', eSel.selectedIndex, a.id, r.width, r.height, w, h);
              for (let i = 1, j = video.height.length; i < j; i++) {
                if (video.height[i] === h) before = video.quality[i];
                if (video.height[i] === r.height) after = video.quality[i];
              }
              if (h !== r.height && !eQua.classList.contains('ar_pre')) {
                log('activequalitychanged 3', eSel.selectedIndex, a.id, r.width, r.height, w, h);
                eQua.classList.add('ar_pre');
                showQuality(`画質変更:${before} → ${after}`);
              } else if (r.height === h) {
                log('activequalitychanged 4', eSel.selectedIndex, a.id, r.width, r.height, w, h);
                eQua.classList.remove('ar_pre');
                showQuality(`画質:${after}`);
                n = 0;
                clearInterval(timer.playerActiveQualityChanged);
              } else if (n > 60) {
                log('activequalitychanged 5', eSel.selectedIndex, a.id, r.width, r.height, w, h);
                n = 0;
                clearInterval(timer.playerActiveQualityChanged);
                eQua.classList.remove('ar_show');
                eQua.classList.remove('ar_pre');
              } else n += 1;
            }
          }
        }, 1000);
      }
    }
  };

  //動画を再読み込みする
  const reloadVideo = s => {
    log(`reloadVideo: ${s}`);
    if (flag.type === 1) {
      log('reloadVideo-1');
      if (theoplayer && theoplayer.player) {
        theoplayer.player(0).load();
        clearInterval(timer.loading);
        timer.loading = setInterval(() => theoplayer.player(0).load(), 1100);
        flag.reload = true;
        showInfo('動画を再読み込みしています…', 10000);
        clearTimeout(timer.reload);
        timer.reload = setTimeout(() => {
          clearInterval(timer.loading);
          if (flag.reload) changeChannel('reloadVideo-1');
        }, 6000);
      }
    } else if (flag.type === 2) {
      log('reloadVideo-2');
      const vi = returnVideo(`reloadVideo ${s}`);
      if (vi && vi.error) changeChannel('reloadVideo-2');
    }
  };

  //THEOplayerのプレイヤーもしくはaudio要素を返す
  const returnAudio = () => {
    if (flag.type === 1) return theoplayer.player(0);
    if (flag.type === 2) {
      const vi = document.getElementsByTagName('video'),
        au = document.getElementsByTagName('audio');
      let n = -1;
      for (let i = 0, j = vi.length; i < j; i++) {
        if (vi[i].src && getComputedStyle(vi[i]).display !== 'none') {
          n = i;
          break;
        }
      }
      if (n >= 0 && au[n].src) return au[n];
      for (let k = 0, l = au.length; k < l; k++) {
        if (au[k].src) return au[k];
      }
    }
    return null;
  };


  //チャンネルロゴ画像の要素を返す
  const returnChLogo = s => {
    const e = document.querySelector('div[style] > img[srcset]');
    if (s) {
      if (e) {
        chId = e.getAttribute('alt');
        log('returnChLogo: chId', chId);
      } else log('returnChLogo: not found ch logo', 'error');
    }
    return e;
  };

  //番組詳細の「月日時分~月日時分」の要素を返す
  const returnProgramTime = () => document.querySelector('div > h2 + p + p');

  //フッターのテキストの要素を返す
  const returnFooterText = () => document.querySelector('button[aria-label] + div + div span > span');

  //再生中のvideo要素を返す
  const returnVideo = s => {
    const vi = document.getElementsByTagName('video');
    if (!vi) log('returnVideo: not found video', s);
    for (let i = 0, j = vi.length; i < j; i++) {
      if (vi[i].src && getComputedStyle(vi[i]).display !== 'none') return vi[i];
    }
    return null;
  };

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

  //セッションストレージに設定を保存する
  const saveSessionStorage = () => sessionStorage.setItem(sid, JSON.stringify(ss));

  //設定の値を用意する
  const setupSettings = () => {
    let di = (Number.isInteger(Number(displayInformation))) ? Number(displayInformation) : 0;
    if (!Number.isInteger(Number(ls.videoQuality))) ls.videoQuality = 0;
    if (!Array.isArray(ls.automaticQualities)) ls.automaticQualities = [true, true, true, true, true];
    di = (di === 0) ? 0 : (di > 2) ? 2 : (di < 1) ? 1 : di;
    displayInformation = (ls.displayInformation) ? ls.displayInformation : (di) ? di : 1;
    if (di && ls.displayInformation !== di) {
      displayInformation = di;
      ls.displayInformation = di;
      saveLocalStorage();
    }
  };

  //情報の要素を表示
  const showInfo = (s, t, b) => {
    if (!b) log('showInfo', s);
    const e = document.getElementById('AutoReload_Info'),
      delay = t > 0 ? t : 2500;
    if (s) {
      e.textContent = s;
      e.classList.remove('ar_hidden');
      e.classList.add('ar_show');
      clearTimeout(timer.showInfo);
      if (t !== -1) {
        timer.showInfo = setTimeout(() => {
          e.classList.remove('ar_show');
          e.classList.add('ar_hidden');
        }, delay);
      }
    } else {
      clearTimeout(timer.showInfo);
      e.classList.remove('ar_show');
      e.classList.add('ar_hidden');
    }
  };

  //画質情報の要素を表示
  const showQuality = s => {
    log('showQuality', s);
    const e = document.getElementById('AutoReload_Quality');
    e.textContent = s;
    e.classList.remove('ar_hidden');
    e.classList.add('ar_show');
    clearTimeout(timer.showQuality);
    timer.showQuality = setTimeout(() => {
      e.classList.remove('ar_show');
      e.classList.add('ar_hidden');
    }, 2500);
  };

  //詳細情報の要素を表示
  const showSpec = () => {
    log('showSpec');
    const e = document.getElementById('AutoReload_Spec'),
      spec = s => {
        if (s) {
          e.innerHTML = s;
          e.classList.remove('ar_hidden');
          e.classList.add('ar_show');
          clearTimeout(timer.showSpec);
          timer.showSpec = setTimeout(() => {
            e.classList.remove('ar_show');
            e.classList.add('ar_hidden');
          }, 8000);
        } else {
          clearTimeout(timer.showSpec);
          e.classList.remove('ar_show');
          e.classList.add('ar_hidden');
        }
      };
    if (flag.type === 1) {
      if (!e.classList.contains('ar_show')) {
        const p = theoplayer.player(0);
        if (p && p.videoTracks && p.videoTracks.item) {
          const vq = p.videoTracks.item(0).qualities,
            aq = p.audioTracks.item(0).qualities,
            vaq = vq.activeQuality,
            aaq = aq.activeQuality,
            vmax = vq.getQualityByID(vq.length - 1),
            amax = aq.getQualityByID(vq.length - 1),
            fr = p.frameRate,
            vc1 = ['avc1.42', 'avc1.4d', 'avc1.64'],
            vc2 = ['Baseline', 'Main', 'High'],
            ac1 = ['mp4a.40.5', 'mp4a.40.2', 'mp4a.69', 'mp4a.6b'],
            ac2 = ['HE-AAC', 'LC-AAC', 'MP3', 'MP3'];
          let s = '<dl>',
            vac = vaq.codecs,
            vmc = vmax.codecs,
            aac = aaq.codecs,
            amc = amax.codecs,
            qa = '',
            qm = '';
          if (vaq.codecs) {
            for (let i = 0, j = vc1.length, a = vaq.codecs.trim().toLowerCase(), m = vmax.codecs.trim().toLowerCase(); i < j; i++) {
              if (a.indexOf(vc1[i]) === 0) vac = `H.264 ${vc2[i]} Profile`;
              if (m.indexOf(vc1[i]) === 0) vmc = `H.264 ${vc2[i]} Profile`;
            }
          }
          if (aaq.codecs) {
            for (let i = 0, j = ac1.length, a = aaq.codecs.trim().toLowerCase(), m = amax.codecs.trim().toLowerCase(); i < j; i++) {
              if (a === ac1[i]) aac = ac2[i];
              if (m === ac1[i]) amc = ac2[i];
            }
          }
          s += `<dt>解像度</dt><dd>${p.videoWidth}×${p.videoHeight}</dd>`;
          s += `<dt>表示サイズ</dt><dd>${p.width}×${p.height}</dd>`;
          if (fr && isFinite(fr)) s += `<dt>フレームレート</dt><dd>${Number(fr.toFixed(3))}fps</dd>`;
          if (vaq.resolution.height) {
            const qn = ss.tempQuality >= 0 ? `*${ss.tempQuality}` : ls.videoQuality;
            qa = ss.tempQuality >= 0 ? `*${video.quality[ss.tempQuality]}` : video.quality[ls.videoQuality];
            s += `<dt>選択中の画質 (${qn})</dt><dd>${qa} (${vaq.resolution.width}×${vaq.resolution.height}, ${vaq.frameRate}fps, ${vaq.bandwidth / 1000}kbps, ${vac} / ${aac})</dd>`;
          }
          if (vmax.resolution.height) {
            for (let i = 1, j = video.height.length; i < j; i++) {
              if (vmax.resolution.height === video.height[i]) qm = video.quality[i];
            }
            s += `<dt>選択可能な最高画質</dt><dd>${qm} (${vmax.resolution.width}×${vmax.resolution.height}, ${vmax.frameRate}fps, ${(vmax.bandwidth / 1000)}kbps, ${vmc} / ${amc})</dd>`;
          }
          if (ls.videoQuality === 0 && ss.tempQuality === -1) {
            s += '<dt>自動最適化画質</dt><dd>';
            for (let i = 1, j = video.quality.length; i < j; i++) {
              if (ls.automaticQualities[i - 1]) s += `${video.quality[i]} `;
            }
            s += '</dd>';
          }
          s += '<dt>配信方式</dt><dd>HTTP Live Streaming</dd>';
          spec(`${s}</dl>`);
        }
      } else spec();
    } else if (flag.type === 2) {
      const v = returnVideo('showSpec');
      if (v && v.videoHeight) {
        let s = `<dl><dt>解像度</dt><dd>${v.videoWidth}×${v.videoHeight}</dd>`;
        s += '<dt>配信方式</dt><dd>MPEG-DASH</dd>';
        if (!e.classList.contains('ar_show')) spec(`${s}</dl>`);
        else spec();
      }
    }
  };

  //指定時間だけ待つ
  const sleep = msec => new Promise(resolve => setTimeout(resolve, msec));

  //ページを開いて動画が表示されたら1度だけ実行
  const startObserve = () => {
    log('startObserve');
    ePrev = document.querySelector('div[aria-hidden] > div[class] > button:first-child');
    eNext = document.querySelector('div[aria-hidden] > div[class] > button:last-child');
    observerC.observe(returnChLogo(), moConfig);
    observerV.observe(returnVideo('startObserve').parentNode, moConfig2);
    observerT.observe(returnFooterText(), moConfig2);
    addEventPage();
    addEventVideo('startObserve');
  };

  //動画の再生速度を元に戻す
  const undoPlaySpeed = s => {
    log('undoPlaySpeed', s);
    const vi = returnVideo('undoPlaySpeed'),
      au = returnAudio();
    checkPlayingVideo('undoPlaySpeed');
    if (vi && vi.playbackRate && vi.playbackRate !== 1) {
      vi.playbackRate = 1;
      if (flag.type === 2 && au) au.playbackRate = 1;
      showInfo(`${vi.playbackRate}倍速`);
    }
  };

  //動画の取得中にエラーが発生したとき
  const videoError = () => {
    log('videoError');
    checkPlayingVideo('videoError');
  };

  //動画を再生し始めたときや再生中にソースが切り替わったとき
  const videoPlaying = () => {
    log('videoPlaying');
    clearInterval(timer.loading);
    flag.reload = false;
    flag.undoChannel = false;
    showInfo();
  };

  //動画を取得できなかったときや取得し終えたとき
  const videoStalled = () => {
    if (!eV || (eV && !(eV.readyState === 4 && eV.networkState === 2))) {
      log('---videoStalled---');
      undoPlaySpeed('videoStalled');
    }
  };

  //動画の読み込みを待っているとき
  const videoWaiting = () => {
    log('---videoWaiting---');
    undoPlaySpeed('videoWaiting');
  };

  //動画を再生できるがキャッシュが足りないとき
  const videoCanplay = () => log('videoCanplay');
  //動画を再生できてキャッシュも足りるとき
  const videoCanplaythrough = () => log('videoCanplaythrough');
  //動画の長さが変更されたとき
  const videoDurationchange = () => {
    if (!eV || (eV && !(eV.readyState === 4 && eV.networkState === 2))) log('videoDurationchange');
  };
  //動画が空になったとき
  const videoEmpied = () => log('videoEmpied');
  //動画を最後まで再生したとき
  const videoEnded = () => log('videoEnded');
  //動画のメタ情報を読み込んだとき
  const videoLoadedmetadata = () => log('videoLoadedmetadata');
  //動画を再生できる状態になったとき
  const videoLoadeddata = () => log('videoLoadeddata');
  //動画をこれから読み込むとき
  const videoLoadstart = () => log('videoLoadstart');
  //動画を一時停止・解除したとき
  const videoPause = () => log('videoPause');
  //動画を(手動操作で)再生し始めたとき
  const videoPlay = () => log('videoPlay');
  //動画を取得しているとき
  const videoProgress = () => {
    if (eV.readyState < 3 || eV.networkState !== 2) log('videoProgress');
  };
  //動画をシークし終えたとき
  const videoSeeked = () => log('===videoSeeked===');
  //動画をシークし始めたとき
  const videoSeeking = () => log('===videoSeeking===');
  //動画の取得を取りやめたとき
  const videoSuspend = () => log('videoSuspend');
  //動画の再生位置が更新されたとき
  const videoTimeupdate = () => {
    if (eV.readyState < 3 || eV.networkState !== 2) log('videoTimeupdate');
  };

  //動画が表示されるのを待つ
  const waitShowVideo = async s => {
    log('waitShowVideo', s, flag.show);
    const splash = () => {
      const sp = document.querySelector('#splash > div');
      if (!sp) return true;
      const cs = getComputedStyle(sp);
      if (cs && cs.display === 'none') return true;
      return false;
    };
    if (!flag.show) {
      flag.show = true;
      clearInterval(timer.waitShowVideo);
      await sleep(400);
      checkVideoFormat();
      eV = returnVideo(`waitShowVideo1 ${s}`);
      if (eV && !isNaN(eV.duration) && splash()) {
        if (s === 'init') startObserve();
        else addEventVideo('waitShowVideo1');
        flag.show = false;
      } else {
        clearInterval(timer.waitShowVideo);
        timer.waitShowVideo = setInterval(() => {
          checkVideoFormat();
          eV = returnVideo(`waitShowVideo2 ${s}`);
          if (eV && !isNaN(eV.duration) && splash()) {
            clearInterval(timer.waitShowVideo);
            flag.countWaitShowVideo = 0;
            if (s === 'init') startObserve();
            else addEventVideo('waitShowVideo2');
            flag.show = false;
          } else if (flag.countWaitShowVideo > 40 && !flag.undoChannel) {
            clearInterval(timer.waitShowVideo);
            flag.countWaitShowVideo = 0;
            changeChannel('waitShowVideo');
          } else flag.countWaitShowVideo += 1;
        }, 200);
      }
    }
    if (s === 'observerC') {
      returnChLogo('waitShowVideo-observerC');
      if (flag.changeChannel && !flag.undoChannel) {
        await sleep(1000);
        log('waitShowVideo: undoChannel');
        flag.changeChannel = false;
        flag.undoChannel = true;
        ePrev.click();
        await sleep(10000);
        flag.undoChannel = false;
      }
    }
  };

  const waitShowVideoC = () => waitShowVideo('observerC'),
    waitShowVideoV = () => waitShowVideo('observerV'),
    observerC = new MutationObserver(waitShowVideoC),
    observerV = new MutationObserver(waitShowVideoV),
    observerT = new MutationObserver(changeProgramTitle);

  clearInterval(timer.init);
  timer.init = setInterval(() => {
    if (/^https:\/\/abema\.tv\/now-on-air\/.+/.test(location.href)) {
      clearInterval(timer.init);
      init();
    }
  }, 1000);

})();