JoyRemocon

Nintendo SwitchのJoy-Conを動画プレイヤーのリモコンにする.

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name        JoyRemocon
// @namespace   https://github.com/segabito/
// @description Nintendo SwitchのJoy-Conを動画プレイヤーのリモコンにする.
// @include     *://*.nicovideo.jp/watch/*
// @include     *://www.youtube.com/*
// @include     *://www.bilibili.com/video/*
// @include     *://www.amazon.co.jp/gp/video/*
// @version     1.6.0
// @author      segabito macmoto
// @license     public domain
// @grant       none
// @noframes
// ==/UserScript==




(() => {

  const monkey = () => {
    if (!window.navigator.getGamepads) {
      window.console.log('%cGamepad APIがサポートされていません', 'background: red; color: yellow;');
      return;
    }

    const PRODUCT = 'JoyRemocon';
    let isPauseButtonDown = false;
    let isRate1ButtonDown = false;
    let isMetaButtonDown = false;

    const getVideo = () => {
      switch (location.host) {
        case 'www.nicovideo.jp':
          return document.querySelector('.MainVideoPlayer video');
        case 'www.amazon.co.jp':
          return document.querySelector('video[width="100%"]');
        default:
          return Array.from(document.querySelectorAll('video')).find(v => {
            return !!v.src;
          });
      }
    };

    const video = {
      get currentTime() {
        try {
          return window.__videoPlayer ?
            __videoplayer.currentTime() : getVideo().currentTime;
        } catch (e) {
          console.warn(e);
          return 0;
        }
      },
      set currentTime(v) {
        try {
          if (v <= video.currentTime && location.host === 'www.nicovideo.jp') {
            return seekNico(v);
          } else if (location.host === 'www.amazon.co.jp') {
            return seekPrimeVideo(v);
          }
          getVideo().currentTime = v;
        } catch (e) {
          console.warn(e);
        }
      },

      get muted() {
        try {
          return getVideo().muted;
        } catch (e) {
          console.warn(e);
          return false;
        }
      },

      set muted(v) {
        try {
          getVideo().muted = v;
        } catch (e) {
          console.warn(e);
        }
      },

      get playbackRate() {
        try {
          return window.__videoPlayer ?
            __videoplayer.playbackRate() : getVideo().playbackRate;
        } catch (e) {
          console.warn(e);
          return 1;
        }
      },
      set playbackRate(v) {
        try {
          if (window.__videoPlayer) {
            window.__videoPlayer.playbackRate(v);
            return;
          }
          getVideo().playbackRate = Math.max(0.01, v);
        } catch (e) {
          console.warn(e);
        }
      },

      get volume() {
        try {
          if (location.host === 'www.nicovideo.jp') {
            return getVolumeNico();
          }
          return getVideo().volume;
        } catch (e) {
          console.warn(e);
          return 1;
        }
      },

      set volume(v) {
        try {
          v = Math.max(0, Math.min(1, v));
          if (location.host === 'www.nicovideo.jp') {
            return setVolumeNico(v);
          }
          getVideo().volume = v;
        } catch (e) {
          console.warn(e);
        }
      },

      get duration() {
        try {
          return getVideo().duration;
        } catch (e) {
          console.warn(e);
          return 1;
        }
      },

      play() {
        try {
          return getVideo().play();
        } catch (e) {
          console.warn(e);
          return Promise.reject();
        }
      },

      pause() {
        try {
          return getVideo().pause();
        } catch (e) {
          console.warn(e);
          return Promise.reject();
        }
      },

      get paused() {
        try {
          return getVideo().paused;
        } catch (e) {
          console.warn(e);
          return true;
        }
      },

    };

    const seekNico = time => {
      const xs = document.querySelector('.SeekBar .XSlider');
      let [min, sec] = document.querySelector`.PlayerPlayTime-duration`.textContent.split(':');
      let duration = min * 60 + sec * 1;
      let left = xs.getBoundingClientRect().left;
      let offsetWidth = xs.offsetWidth;
      let per = time / duration * 100;
      let clientX = offsetWidth * per / 100 + left;
      xs.dispatchEvent(new MouseEvent('mousedown', {clientX}));
      document.dispatchEvent(new MouseEvent('mouseup', {clientX}));
    };

    const setVolumeNico = vol => {
      const xs = document.querySelector('.VolumeBar .XSlider');
      let left = xs.getBoundingClientRect().left;
      let offsetWidth = xs.offsetWidth;
      let per = vol * 100;
      let clientX = offsetWidth * per / 100 + left;
      xs.dispatchEvent(new MouseEvent('mousedown', {clientX}));
      document.dispatchEvent(new MouseEvent('mouseup', {clientX}));
    };

    const seekPrimeVideo = time => {
      const xs = document.querySelector('.seekBar .progressBarContainer');
      xs.closest('.bottomPanelItem').style.display = '';
      let left = xs.getBoundingClientRect().left;
      let offsetWidth = xs.offsetWidth;
      let per = (time - 10) / video.duration * 100; // 何故か10秒分ズレてる?
      let clientX = offsetWidth * per / 100 + left;
      // console.log('seek', video.currentTime, time, left, offsetWidth, per, clientX);
      xs.dispatchEvent(new PointerEvent('pointerdown', {clientX}));
      xs.dispatchEvent(new PointerEvent('pointerup', {clientX}));
    };

    const getVolumeNico = () => {
      try {
        const xp = document.querySelector('.VolumeBar .XSlider .ProgressBar-inner');
        return (xp.style.transform || '1').replace(/scaleX\(([0-9\.]+)\)/, '$1') * 1;
      } catch (e) {
        console.warn(e);
        return 1;
      }
    };

    const execCommand = (command, param) => {
      switch (command) {
        case 'playbackRate':
          video.playbackRate = param;
          break;
        case 'toggle-play': {
          const btn = document.querySelector(
            '.ytp-ad-skip-button, .PlayerPlayButton, .PlayerPauseButton, .html5-main-videom, .bilibili-player-video-btn-start, .pausedOverlay');
          if (btn) {
            if (location.host === 'www.amazon.co.jp') {
              btn.dispatchEvent(new CustomEvent('pointerup'));
            } else {
              btn.click();
            }
          } else
          if (video.paused) {
            video.play();
          } else {
            video.pause();
          }
          break;
        }
        case 'toggle-mute': {
          const btn = document.querySelector(
            '.MuteVideoButton, .UnMuteVideoButton, .ytp-mute-button, .bilibili-player-iconfont-volume-max');
          if (btn) {
            btn.click();
          } else {
            video.muted = !video.muted;
          }
          break;
        }
        case 'seek':
          video.currentTime = param * 1;
          break;
        case 'seekBy':
          video.currentTime += param * 1;
          break;
        case 'seekNextFrame':
          video.currentTime += 1 / 60;
          break;
        case 'seekPrevFrame':
          video.currentTime -= 1 / 60;
          break;
        case 'volumeUp': {
          let v = video.volume;
          let r = v < 0.05 ? 1.3 : 1.1;
          video.volume = Math.max(0.05, v * r + 0.01);
          break;
        }
        case 'volumeDown': {
          let v = video.volume;
          let r = 1 / 1.2;
          video.volume = Math.max(0.01, v * r);
          break;
        }
        case 'toggle-showComment': {
          const btn = document.querySelector('.CommentOnOffButton, .bilibili-player-video-danmaku-switch input');
          if (btn) {
            btn.click();
          }
          break;
        }
        case 'toggle-fullscreen': {
          const btn = document.querySelector(
            '.EnableFullScreenButton, .DisableFullScreenButton, .ytp-fullscreen-button, .bilibili-player-video-btn-fullscreen, .imageButton.fullscreenButton');
          if (btn) {
            btn.click();
          }
          break;
        }
        case 'playNextVideo': {
          const btn = document.querySelector(
            '.PlayerSkipNextButton, .ytp-next-button, .nextTitleButton, .skipAdButton');
          if (btn) {
            btn.click();
          }
          break;
        }
        case 'playPreviousVideo': {
          const btn = document.querySelector(
            '.PlayerSeekBackwardButton');
          if (btn) {
            btn.click();
          }
          if (['www.youtube.com'].includes(location.host)) {
            history.back();
          }
          break;
        }
        case 'screenShot': {
          screenShot();
          break;
        }
        case 'deflistAdd': {
          const btn = document.querySelector(
            '.InstantMylistButton');
          if (btn) {
            btn.click();
          }
          break;
        }
        case 'notify':
          notify(param);
          break;
        case 'unlink':
          if (document.hasFocus()) {
            JoyRemocon.unlink();
          }
          break;
        default:
          console.warn('unknown command "%s" "%o"', command, param);
          break;
      }
    };

    const notify = message => {
      const div = document.createElement('div');
      div.textContent = message;
      Object.assign(div.style, {
        position: 'fixed',
        display: 'inline-block',
        zIndex: 1000000,
        left: 0,
        bottom:  0,
        transition: 'opacity 0.4s linear, transform 0.5s ease',
        padding: '8px 16px',
        background: '#00c',
        color: 'rgba(255, 255, 255, 0.8)',
        fontSize: '16px',
        fontWeight: 'bolder',
        whiteSpace: 'nowrap',
        textAlign: 'center',
        boxShadow: '2px 2px 0 #ccc',
        userSelect: 'none',
        pointerEvents: 'none',
        willChange: 'transform',
        opacity: 0,
        transform: 'translate(0, +100%) translate(48px, +48px) ',
      });


      const parent = document.querySelector('.MainContainer') || document.body;
      parent.append(div);

      setTimeout(() => {
        Object.assign(div.style, { opacity: 1, transform: 'translate(48px, -48px)' });
      }, 100);
      setTimeout(() => {
        Object.assign(div.style, { opacity: 0, transform: 'translate(48px, -48px) scaleY(0)' });
      }, 2000);
      setTimeout(() => {
        div.remove();
      }, 3000);
    };

    const getVideoTitle = () => {
      switch (location.host) {
        case 'www.nicovideo.jp':
          return document.title;
        case 'www.youtube.com':
          return document.title;
        default:
          return document.title;
      }
    };

    const toSafeName = function(text) {
      text = text.trim()
        .replace(/</g, '<')
        .replace(/>/g, '>')
        .replace(/\?/g, '?')
        .replace(/:/g, ':')
        .replace(/\|/g, '|')
        .replace(/\//g, '/')
        .replace(/\\/g, '¥')
        .replace(/"/g, '”')
        .replace(/\./g, '.')
      ;
      return text;
    };

    const speedUp = () => {
      let current = video.playbackRate;
      execCommand('playbackRate', Math.floor(Math.min(current + 0.1, 3) * 10) / 10);
    };

    const speedDown = () => {
      let current = video.playbackRate;
      execCommand('playbackRate', Math.floor(Math.max(current - 0.1, 0.1) * 10) / 10);
    };

    const scrollUp = () => {
      document.documentElement.scrollTop =
        Math.max(0, document.documentElement.scrollTop - window.innerHeight / 5);
    };

    const scrollDown = () => {
      document.documentElement.scrollTop =
        document.documentElement.scrollTop + window.innerHeight / 5;
    };

    const scrollToVideo = () => {
      getVideo().scrollIntoView({behavior: 'smooth', block: 'center'});
    };

    const screenShot = video => {
      video = video || getVideo();
      if (!video) {
        return;
      }
      // draw canvas
      const width = video.videoWidth;
      const height = video.videoHeight;
      const canvas = document.createElement('canvas');
      canvas.width = width;
      canvas.height = height;
      const context = canvas.getContext('2d');
      context.drawImage(video, 0, 0);
      document.body.append(canvas);
      // fileName
      const videoTitle = getVideoTitle();
      const currentTime = video.currentTime;
      const min = Math.floor(currentTime / 60);
      const sec = (currentTime % 60 + 100).toString().substr(1, 6);
      const time = `${min}_${sec}`;
      const fileName = `${toSafeName(videoTitle)}@${time}.png`;

      // to objectURL
      console.time('canvas to DataURL');
      const dataURL = canvas.toDataURL('image/png');
      console.timeEnd('canvas to DataURL');

      console.time('dataURL to objectURL');
      const bin = atob(dataURL.split(',')[1]);
      const buf = new Uint8Array(bin.length);
      for (let i = 0, len = buf.length; i < len; i++) {
        buf[i] = bin.charCodeAt(i);
      }
      const blob = new Blob([buf.buffer], {type: 'image/png'});
      const objectURL = URL.createObjectURL(blob);
      console.timeEnd('dataURL to objectURL');

      // save
      const link = document.createElement('a');
      link.setAttribute('download', fileName);
      link.setAttribute('href', objectURL);
      document.body.append(link);
      link.click();
      setTimeout(() => { link.remove(); URL.revokeObjectURL(objectURL); }, 1000);
    };

    const ButtonMapJoyConL = {
      Y: 0,
      B: 1,
      X: 2,
      A: 3,
      SUP: 4,
      SDN: 5,
      SEL: 8,
      CAP: 13,
      LR: 14,
      META: 15,
      PUSH: 10
    };
    const ButtonMapJoyConR = {
      Y: 3,
      B: 2,
      X: 1,
      A: 0,
      SUP: 5,
      SDN: 4,
      SEL: 9,
      CAP: 12,
      LR: 14,
      META: 15,
      PUSH: 11
    };

    const JoyConAxisCenter = +1.28571;

    const AxisMapJoyConL = {
      CENTER: JoyConAxisCenter,
      UP:     +0.71429,
      U_R:    +1.00000,
      RIGHT:  -1.00000,
      D_R:    -0.71429,
      DOWN:   -0.42857,
      D_L:    -0.14286,
      LEFT:   +0.14286,
      U_L:    +0.42857,
    };

    const AxisMapJoyConR = {
      CENTER: JoyConAxisCenter,
      UP:     -0.42857,
      U_R:    -0.14286,
      RIGHT:  +0.14286,
      D_R:    +0.42857,
      DOWN:   +0.71429,
      D_L:    +1.00000,
      LEFT:   -1.00000,
      U_L:    -0.71429,
    };


    const onButtonDown = (button, deviceId) => {
      const ButtonMap = deviceId.match(/Vendor: 057e Product: 2006/i) ?
        ButtonMapJoyConL : ButtonMapJoyConR;
      switch (button) {
        case ButtonMap.Y:
          if (isPauseButtonDown) {
            execCommand('seekPrevFrame');
          } else {
            execCommand('toggle-showComment');
          }
          break;
        case ButtonMap.B:
          isPauseButtonDown = true;
          execCommand('toggle-play');
          break;
        case ButtonMap.X:
          if (isMetaButtonDown) {
            execCommand('playbackRate', 2);
          } else {
            isRate1ButtonDown = true;
            execCommand('playbackRate', 0.1);
          }
          break;
        case ButtonMap.A:
          if (isPauseButtonDown) {
            execCommand('seekNextFrame');
          } else {
            execCommand('toggle-mute');
          }
          break;
        case ButtonMap.SUP:
          if (isMetaButtonDown) {
            scrollUp();
          } else {
            execCommand('playPreviousVideo');
          }
          break;
        case ButtonMap.SDN:
          if (isMetaButtonDown) {
            scrollDown();
          } else {
            execCommand('playNextVideo');
          }
          break;
        case ButtonMap.SEL:
          if (isMetaButtonDown) {
            execCommand('unlink');
          } else {
            execCommand('deflistAdd');
          }
          break;
        case ButtonMap.CAP:
          if (location.host === 'www.amazon.co.jp') {
            return;
          }
          execCommand('screenShot');
          break;
        case ButtonMap.PUSH:
          if (isMetaButtonDown) {
            scrollToVideo();
          } else {
            execCommand('seek', 0);
          }
          break;
        case ButtonMap.LR:
          execCommand('toggle-fullscreen');
          break;
        case ButtonMap.META:
          isMetaButtonDown = true;
          break;
      }
    };


    const onButtonUp = (button, deviceId) => {
      const ButtonMap = deviceId.match(/Vendor: 057e Product: 2006/i) ?
        ButtonMapJoyConL : ButtonMapJoyConR;
      switch (button) {
        case ButtonMap.Y:
          break;
        case ButtonMap.B:
          isPauseButtonDown = false;
          break;
        case ButtonMap.X:
          isRate1ButtonDown = false;
          execCommand('playbackRate', 1);
          break;
        case ButtonMap.META:
          isMetaButtonDown = false;
          break;
      }
    };


    const onButtonRepeat = (button, deviceId) => {
      const ButtonMap = deviceId.match(/Vendor: 057e Product: 2006/i) ?
        ButtonMapJoyConL : ButtonMapJoyConR;
      switch (button) {
        case ButtonMap.Y:
          if (isMetaButtonDown) {
            execCommand('seekBy', -15);
          } else if (isPauseButtonDown) {
            execCommand('seekPrevFrame');
          }
          break;

        case ButtonMap.A:
          if (isMetaButtonDown) {
            execCommand('seekBy', 15);
          } else if (isPauseButtonDown) {
            execCommand('seekNextFrame');
          }
          break;
        case ButtonMap.SUP:
          if (isMetaButtonDown) {
            scrollUp();
          } else {
            execCommand('playPreviousVideo');
          }
          break;
        case ButtonMap.SDN:
          if (isMetaButtonDown) {
            scrollDown();
          } else {
            execCommand('playNextVideo');
          }
          break;
      }
    };


    const onAxisChange = (axis, value, deviceId) => {};
    const onAxisRepeat = (axis, value, deviceId) => {};
    const onPovChange = (pov, deviceId) => {
      switch(pov) {
        case 'UP':
          if (isMetaButtonDown) {
            speedUp();
          } else {
            execCommand('volumeUp');
          }
          break;
        case 'DOWN':
          if (isMetaButtonDown) {
            speedDown();
          } else {
            execCommand('volumeDown');
          }
          break;
        case 'LEFT':
          execCommand('seekBy', isRate1ButtonDown || isMetaButtonDown ? -1 : -5);
          break;
        case 'RIGHT':
          execCommand('seekBy', isRate1ButtonDown || isMetaButtonDown ? +1 : +5);
          break;
      }
    };

    const onPovRepeat = onPovChange;


    class Handler {
      constructor(...args) {
        this._list = new Array(...args);
      }

      get length() {
        return this._list.length;
      }

      exec(...args) {
        if (!this._list.length) {
          return;
        } else if (this._list.length === 1) {
          this._list[0](...args);
          return;
        }
        for (let i = this._list.length - 1; i >= 0; i--) {
          this._list[i](...args);
        }
      }

      execMethod(name, ...args) {
        if (!this._list.length) {
          return;
        } else if (this._list.length === 1) {
          this._list[0][name](...args);
          return;
        }
        for (let i = this._list.length - 1; i >= 0; i--) {
          this._list[i][name](...args);
        }
      }

      add(member) {
        if (this._list.includes(member)) {
          return this;
        }
        this._list.unshift(member);
        return this;
      }

      remove(member) {
        _.pull(this._list, member);
        return this;
      }

      clear() {
        this._list.length = 0;
        return this;
      }

      get isEmpty() {
        return this._list.length < 1;
      }
    }


    const {Emitter} = (() => {
      class Emitter {

        on(name, callback) {
          if (!this._events) {
            Emitter.totalCount++;
            this._events = {};
          }

          name = name.toLowerCase();
          let e = this._events[name];
          if (!e) {
            e = this._events[name] = new Handler(callback);
          } else {
            e.add(callback);
          }
          if (e.length > 10) {
            Emitter.warnings.push(this);
          }
          return this;
        }

        off(name, callback) {
          if (!this._events) {
            return;
          }

          name = name.toLowerCase();
          const e = this._events[name];

          if (!this._events[name]) {
            return;
          } else if (!callback) {
            delete this._events[name];
          } else {
            e.remove(callback);

            if (e.isEmpty) {
              delete this._events[name];
            }
          }

          if (Object.keys(this._events).length < 1) {
            delete this._events;
          }
          return this;
        }

        once(name, func) {
          const wrapper = (...args) => {
            func(...args);
            this.off(name, wrapper);
            wrapper._original = null;
          };
          wrapper._original = func;
          return this.on(name, wrapper);
        }

        clear(name) {
          if (!this._events) {
            return;
          }

          if (name) {
            delete this._events[name];
          } else {
            delete this._events;
            Emitter.totalCount--;
          }
          return this;
        }

        emit(name, ...args) {
          if (!this._events) {
            return;
          }

          name = name.toLowerCase();
          const e = this._events[name];

          if (!e) {
            return;
          }

          e.exec(...args);
          return this;
        }

        emitAsync(...args) {
          if (!this._events) {
            return;
          }

          setTimeout(() => {
            this.emit(...args);
          }, 0);
          return this;
        }
      }

      Emitter.totalCount = 0;
      Emitter.warnings = [];

      return {
        Emitter
      };
    })();

    class PollingTimer {
      constructor(callback, interval) {
        this._timer = null;
        this._callback = callback;
        if (typeof interval === 'number') {
          this.changeInterval(interval);
        }
      }
      changeInterval(interval) {
        if (this._timer) {
          if (this._currentInterval === interval) {
            return;
          }
          window.clearInterval(this._timer);
        }
        console.log('%cupdate Interval:%s', 'background: lightblue;', interval);
        this._currentInterval = interval;
        this._timer = window.setInterval(this._callback, interval);
      }
      pause() {
        window.clearInterval(this._timer);
        this._timer = null;
      }
      start() {
        if (typeof this._currentInterval !== 'number') {
          return;
        }
        this.changeInterval(this._currentInterval);
      }
    }

    class GamePad extends Emitter {
      constructor(gamepadStatus) {
        super();
        this._gamepadStatus = gamepadStatus;
        this._buttons = [];
        this._axes = [];
        this._pov = '';
        this._lastTimestamp = 0;
        this._povRepeat = 0;
        this.initialize(gamepadStatus);
      }
      initialize(gamepadStatus) {
        this._buttons.length = gamepadStatus.buttons.length;
        this._axes.length = gamepadStatus.axes.length;
        this._id = gamepadStatus.id;
        this._index = gamepadStatus.index;
        this._isRepeating = false;
        this.reset();
      }
      reset() {
        let i, len;
        this._pov = '';
        this._povRepeat = 0;

        for (i = 0, len = this._gamepadStatus.buttons.length + 16; i < len; i++) {
          this._buttons[i] = {pressed: false, repeat: 0};
        }
        for (i = 0, len = this._gamepadStatus.axes.length; i < len; i++) {
          this._axes[i] = {value: null, repeat: 0};
        }
      }

      update() {
        let gamepadStatus = (navigator.getGamepads())[this._index];

        if (!gamepadStatus || !gamepadStatus.connected) { console.log('no status'); return; }

        if (!this._isRepeating && this._lastTimestamp === gamepadStatus.timestamp) {
          return;
        }
        this._gamepadStatus = gamepadStatus;
        this._lastTimestamp = gamepadStatus.timestamp;

        let buttons = gamepadStatus.buttons, axes = gamepadStatus.axes;
        let i, len, axis, isRepeating = false;

        for (i = 0, len = Math.min(this._buttons.length, buttons.length); i < len; i++) {
          let buttonStatus = buttons[i].pressed ? 1 : 0;

          if (this._buttons[i].pressed !== buttonStatus) {
            let eventName = (buttonStatus === 1) ? 'onButtonDown' : 'onButtonUp';
            this.emit(eventName, i, 0);
            this.emit('onButtonStatusChange', i, buttonStatus);
          }
          this._buttons[i].pressed = buttonStatus;
          if (buttonStatus) {
            this._buttons[i].repeat++;
            isRepeating = true;
            if (this._buttons[i].repeat % 5 === 0) {
              //console.log('%cbuttonRepeat%s', 'background: lightblue;', i);
              this.emit('onButtonRepeat', i);
            }
          } else {
            this._buttons[i].repeat = 0;
          }
        }
        for (i = 0, len = Math.min(8, this._axes.length); i < len; i++) {
          axis = Math.round(axes[i] * 1000) / 1000;

          if (this._axes[i].value === null) {
            this._axes[i].value = axis;
            continue;
          }

          let diff = Math.round(Math.abs(axis - this._axes[i].value));
          if (diff >= 1) {
            this.emit('onAxisChange', i, axis);
          }
          if (Math.abs(axis) <= 0.1 && this._axes[i].repeat > 0) {
            this._axes[i].repeat = 0;
          } else if (Math.abs(axis) > 0.1) {
            this._axes[i].repeat++;
            isRepeating = true;
          } else {
            this._axes[i].repeat = 0;
          }
          this._axes[i].value = axis;

        }

        if (typeof axes[9] !== 'number') {
          this._isRepeating = isRepeating;
          return;
        }
        {
          const b = 100000;
          const axis = Math.trunc(axes[9] * b);
          const margin = b / 10;
          let pov = '';
          const AxisMap = this._id.match(/Vendor: 057e Product: 2006/i) ? AxisMapJoyConL : AxisMapJoyConR;
          if (Math.abs(JoyConAxisCenter * b - axis) <= margin) {
            pov = '';
          } else {
            Object.keys(AxisMap).forEach(key => {
              if (Math.abs(AxisMap[key] * b - axis) <= margin) {
                pov = key;
              }
            });
          }
          if (this._pov !== pov) {
            this._pov = pov;
            this._povRepeat = 0;
            isRepeating = pov !== '';
            this.emit('onPovChange', this._pov);
          } else if (pov !== '') {
            this._povRepeat++;
            isRepeating = true;
            if (this._povRepeat % 5 === 0) {
              this.emit('onPovRepeat', this._pov);
            }
          }
         }


        this._isRepeating = isRepeating;
      }

      dump() {
        let gamepadStatus = this._gamepadStatus, buttons = gamepadStatus.buttons, axes = gamepadStatus.axes;
        let i, len, btmp = [], atmp = [];
        for (i = 0, len = axes.length; i < len; i++) {
          atmp.push('ax' + i + ': ' + axes[i]);
        }
        for (i = 0, len = buttons.length; i < len; i++) {
          btmp.push('bt' + i + ': ' + (buttons[i].pressed ? 1 : 0));
        }
        return atmp.join('\n') + '\n' + btmp.join(', ');
      }

      getButtonStatus(index) {
        return this._buttons[index] || 0;
      }

      getAxisValue(index) {
        return this._axes[index] || 0;
      }

      release() {
        this.clear();
      }

      get isConnected() {
        return this._gamepadStatus.connected ? true : false;
      }

      get deviceId() {
        return this._id;
      }

      get deviceIndex() {
        return this._index;
      }

      get buttonCount() {
        return this._buttons ? this._buttons.length : 0;
      }

      get axisCount() {
        return this._axes ? this._axes.length : 0;
      }

      get pov() {
        return this._pov;
      }

      get x() {
        return this._axes.length > 0 ? this._axes[0] : 0;
      }

      get y() {
        return this._axes.length > 1 ? this._axes[1] : 0;
      }

      get z() {
        return this._axes.length > 2 ? this._axes[2] : 0;
      }
    }

    const noop = () => {};

    const JoyRemocon = (() => {
      let activeGamepad = null;
      let pollingTimer = null;
      let emitter = new Emitter();
      let unlinked = false;

      const detectGamepad = () => {
        if (activeGamepad) {
          return;
        }
        const gamepads = navigator.getGamepads();
        if (gamepads.length < 1) {
          return;
        }
        const pad = Array.from(gamepads).reverse().find(pad => {
          return  pad &&
                  pad.connected &&
                  pad.id.match(/^Joy-Con/i);
        });
        if (!pad) { return; }

        window.console.log(
          '%cdetect gamepad index: %s, id: "%s", buttons: %s, axes: %s',
          'background: lightgreen; font-weight: bolder;',
          pad.index, pad.id, pad.buttons.length, pad.axes.length
        );

        const gamepad = new GamePad(pad);
        activeGamepad = gamepad;

        gamepad.on('onButtonDown',
            number => emitter.emit('onButtonDown', number, gamepad.deviceIndex));
        gamepad.on('onButtonRepeat',
            number => emitter.emit('onButtonRepeat', number, gamepad.deviceIndex));
        gamepad.on('onButtonUp',
            number => emitter.emit('onButtonUp', number, gamepad.deviceIndex));
        gamepad.on('onPovChange',
            pov => emitter.emit('onPovChange', pov, gamepad.deviceIndex));
        gamepad.on('onPovRepeat',
            pov => emitter.emit('onPovRepeat', pov, gamepad.deviceIndex));

        emitter.emit('onDeviceConnect', gamepad.deviceIndex, gamepad.deviceId);

        pollingTimer.changeInterval(30);
      };


      const onGamepadConnectStatusChange = (e, isConnected) => {
        console.log('onGamepadConnetcStatusChange', e, e.gamepad.index, isConnected);

        if (isConnected) {
          console.log('%cgamepad connected id:"%s"', 'background: lightblue;', e.gamepad.id);
          detectGamepad();
        } else {
          emitter.emit('onDeviceDisconnect', activegamepad.deviceIndex);
          // if (activeGamepad) {
          //   activeGamepad.release();
          // }
          // activeGamepad = null;
          console.log('%cgamepad disconneced id:"%s"', 'background: lightblue;', e.gamepad.id);
        }
      };

      const initializeTimer = () => {
        console.log('%cinitializeGamepadTimer', 'background: lightgreen;');

        const onTimerInterval = () => {
          if (unlinked) {
            return;
          }
          if (!activeGamepad) {
            return detectGamepad();
          }
          if (!activeGamepad.isConnected) {
            return;
          }
          activeGamepad.update();
        };

        pollingTimer = new PollingTimer(onTimerInterval, 1000);
      };

      const initializeGamepadConnectEvent = () => {
        console.log('%cinitializeGamepadConnectEvent', 'background: lightgreen;');

        window.addEventListener('gamepadconnected',
          function(e) { onGamepadConnectStatusChange(e, true); });
        window.addEventListener('gamepaddisconnected',
          function(e) { onGamepadConnectStatusChange(e, false); });

        if (activeGamepad) {
          return;
        }
        window.setTimeout(detectGamepad, 1000);
      };


      let hasStartDetect = false;
      return {
        on: (...args) => { emitter.on(...args); },
        startDetect: () => {
          if (hasStartDetect) { return; }
          hasStartDetect = true;
          initializeTimer();
          initializeGamepadConnectEvent();
        },
        startPolling: () => {
          if (pollingTimer) { pollingTimer.start(); }
        },
        stopPolling: () => {
          if (pollingTimer) { pollingTimer.pause(); }
        },
        unlink: () => {
          if (!activeGamepad) {
            return;
          }
          unlinked = true;
          activeGamepad.release();
          activeGamepad = null;
          pollingTimer.changeInterval(1000);
          execCommand(
            'notify',
            'JoyRemocon と切断しました'
          );
          }
      };
    })();


    const initGamepad = () => {

      let isActivated = false;
      let deviceId, deviceIndex;

      let notifyDetect = () =>  {
        if (!document.hasFocus()) { return; }
        isActivated = true;
        notifyDetect = noop;

        // 初めてボタンかキーが押されたタイミングで通知する
        execCommand(
          'notify',
          'ゲームパッド "' + deviceId + '" とリンクしました'
        );
      };


      let bindEvents = () => {
        bindEvents = noop;

        JoyRemocon.on('onButtonDown',   number => {
          notifyDetect();
          if (!isActivated) { return; }
          onButtonDown(number, deviceId);
        });
        JoyRemocon.on('onButtonRepeat', number => {
          if (!isActivated) { return; }
          onButtonRepeat(number, deviceId);
        });
        JoyRemocon.on('onButtonUp',     number => {
          if (!isActivated) { return; }
          onButtonUp(number, deviceId);
        });
        JoyRemocon.on('onPovChange',   pov => {
          if (!isActivated) { return; }
          onPovChange(pov, deviceId);
        });
        JoyRemocon.on('onPovRepeat',   pov => {
          if (!isActivated) { return; }
          onPovRepeat(pov, deviceId);
        });
      };

      let onDeviceConnect = function(index, id) {
         deviceIndex = index;
         deviceId = id;

         bindEvents();
      };

      JoyRemocon.on('onDeviceConnect', onDeviceConnect);
      JoyRemocon.startDetect();
    };


    const initialize = () => {
      initGamepad();
    };

    initialize();
  };

  const script = document.createElement('script');
  script.id = 'JoyRemoconLoader';
  script.setAttribute('type', 'text/javascript');
  script.setAttribute('charset', 'UTF-8');
  script.appendChild(document.createTextNode(`(${monkey})();`));
  document.documentElement.append(script);

})();