LoginSignup
1
1

More than 5 years have passed since last update.

Video API と Canvas で遅れ鏡

Posted at

遅れ鏡とは「 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()
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1