Help us understand the problem. What is going on with this article?

Video API と Canvas で遅れ鏡

More than 1 year has passed since last update.

遅れ鏡とは「 N 秒前のビデオカメラの映像」をリアルタイムに(?)再生するプログラムの通称です、たぶん。
これを JavaScript で実装したものがフォルダ整理していたら出てきました。昔作ったものなのでおかしなところがあるかもしれませんが、もったいないので公開しておきます。

動くデモはこちら

ブラウザから「カメラ撮ってもいーい?」と許可を求められるので求められるままに許可してください。
しばらくすると 見たくもない 自分の顔が遅れて表示されると思います。周囲を警戒しつつ横顔とか後頭部とか確認しましょう。
なお Chrome と Firefox 以外では試していません。

説明

話は簡単で、 navigator.mediaDevices.getUserMedia でカメラの制御権を取得し、一定間隔ごとに静止画像として保存、これを一定時間後に Canvas の drawImage で描画しているだけです。
カメラのサイズなどはストリームの getVideoTracks()[0].getSettings() を使って取得しています。
(このあたりは API の仕様が固まっていない印象を受けたので、現在はより良いアプローチがあるかもしれません。)

コード

GitHub はこちら

HTML

<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://mimonelu.github.io/motoko-css/motoko.css" rel="stylesheet">
<title>Delay Mirror</title>
<style>

  .canvas {
    background-color: #c0c0c0;
    display: block;
    width: 100%;
  }

  .video {
    display: none;
  }

</style>
<body class="motoko">
  <header>
    <h1>Delay Mirror</h1>
    <p>遅れ鏡です。N秒前のフロントカメラの映像を表示します。</p>
  </header>
  <article>
    <aside>
      <label><input name="delay" type="radio" value="0"> 0秒前(遅延なし)</label>
      <label><input name="delay" type="radio" value="3000" checked> 3秒前</label>
      <label><input name="delay" type="radio" value="6000"> 6秒前</label>
      <label><input name="delay" type="radio" value="12000"> 12秒前</label>
    </aside>
    <canvas class="canvas"></canvas>
    <video class="video" playsinline></video>
  </article>
  <footer>
    <div>&copy; 2019 mimonelu</div>
  </footer>
  <script src="./index.js"></script>
</body>

JavaScript

class App {

  constructor () {
    this.lazyCanvas = new LazyCanvas('.canvas')
    this.videoCamera = new VideoCamera('.video')
    this.videoCamera.start().then(() => {
      this.ui = new UI()
      this.ui.bindDelayButton((delay) => { this.start(delay) })
      this.start(3000)
    })
  }

  start (delay) {
    this.lazyCanvas.clear()
    this.lazyCanvas.startRecord(this.videoCamera)
    this.lazyCanvas.startRender(delay)
  }

}

class LazyCanvas {

  constructor (selector) {
    this.canvas = document.querySelector(selector)
    this.context = this.canvas.getContext('2d')
    this.interval = 100
    this.snapshots = []
    this.recordTimer = null
    this.renderTimer = null
  }

  clear () {
    this.context.clearRect(0, 0, this.canvas.width, this.canvas.height)
    this.snapshots = []
    if (this.recordTimer != null) {
      clearInterval(this.recordTimer)
      this.recordTimer = null
    }
    if (this.renderTimer != null) {
      clearInterval(this.renderTimer)
      this.renderTimer = null
    }
  }

  startRecord (videoCamera) {
    const { width, height } = videoCamera.stream.getVideoTracks()[0].getSettings()
    this.recordTimer = setInterval(() => { this.addSnapshot(videoCamera.node, width, height) }, this.interval)
  }

  addSnapshot (videoNode, videoWidth, videoHeight) {
    const canvas = document.createElement('canvas')
    const context = canvas.getContext('2d')
    const height = Math.round(this.canvas.clientWidth * videoHeight / videoWidth)
    canvas.style['width'] = `${this.canvas.clientWidth}px`
    canvas.style['height'] = `${height}px`
    this.canvas.style['height'] = `${height}px`
    context.drawImage(videoNode, 0, 0, canvas.width, canvas.height)
    this.snapshots.push(canvas)
  }

  startRender (delay) {
    setTimeout(() => {
      this.renderTimer = setInterval(() => {
        this.context.drawImage(this.snapshots.shift(), 0, 0)
      }, this.interval)
    }, delay)
  }

}

class VideoCamera {

  constructor (selector) {
    this.node = document.querySelector(selector)
    this.stream = null
    this.fallback()
  }

  fallback () {
    if (navigator.mediaDevices == null) {
      navigator.mediaDevices = {}
    }
    if (navigator.mediaDevices.getUserMedia == null) {
      navigator.mediaDevices.getUserMedia =
        navigator.getUserMedia ||
        navigator.mozGetUserMedia ||
        navigator.webkitGetUserMedia
    }
  }

  start () {
    return navigator.mediaDevices.getUserMedia({
      audio: false,
      video: true
    }).then((stream) => {
      this.node.onloadedmetadata = () => { this.node.play() }
      this.node.srcObject = this.stream = stream
    }).catch((error) => {
      alert(`カメラへのアクセスが拒否された、もしくは何らかのエラーが発生しました。\nエラー: ${error.name}`)
    })
  }

}

class UI {

  bindDelayButton (callback) {
    document.getElementsByName('delay').forEach((node) => {
      node.addEventListener('click', (event) => {
        callback(parseInt(event.target.value, 10))
      })
    })
  }

}

new App()
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away