「あなたの過去を表示します」JavaScriptでWebカメラの画像を遅延表示してみた

#なぜ、過去を表示したいのか
　カメラで撮影した画像を遅れて表示させる方法を「タイム　シフト」「追いかけ再生」と言います。遅延表示により、再生を戻すことなく数秒前の過去の画像を連続して表示し続けます。この機能を使い、例えば保健体育では、跳び箱を跳ぶす様子を撮影しながら遅延再生して自分の動きを確認させることができます。過去に、Windows用ソフトを開発し公開していました。
　しかし、GIGAスクールの時代、学校に導入されるのは、Windows、iOS、chromebookと多種多様です。そこで、どの機種にも対応できるよう、ブラウザ上で作動する「タイムシフトカメラ」を作ってみました。
作動例→  https://kaihatuiinkai.jp/time_shift/in_camera.html

　それでは、作成ステップを説明します。データの流れとプログラム作動を確認してもらうため、ソースコードを全て貼り付けしました。長くなってしまうことをご了承願います。

#カメラ画像を表示
参照：[HTML5] カメラをJSで操作し写真を撮影する
https://blog.katsubemakito.net/html5/camera1
こちらのソースをほぼそのまま利用しています。

カメラ画像を映像要素&lt;video&gt;に表示します。
作動例→  https://kaihatuiinkai.jp/time_shift/camera1.html
なお、カメラ画像を表示させるためには https であることが必要です。

```camera1.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Camera1</title>
  <style>
  canvas, video{ border: 1px solid gray; }
  </style>
</head>
<body>
<h1>カメラ画像を映像要素&lt;video&gt;に表示</h1>
video<br>
<video id="video" width="300" height="200"></video>
<script>
window.onload = () => {
  const video   = document.querySelector("#video");
  // カメラ設定  //
  const constraints = {
    audio: false,
    video: {
      width:  300,
      height: 200,
      facingMode: "user"   // フロントカメラを利用
      // facingMode: { exact: "environment" }  // リアカメラを利用
    }
  };
  // カメラを<video>と同期 //
  navigator.mediaDevices.getUserMedia(constraints)
  .then( (stream) => {
    video.srcObject = stream;
    video.onloadedmetadata = (e) => {
      video.play();
    };
  })
  .catch( (err) => {
    console.log(err.name + ": " + err.message);
  });
};
</script>
</body>
</html>
```

#映像要素の画像を&lt;canvas&gt;に表示
画像を&lt;canvas&gt;に1秒ごとに貼り付けて表示します。
作動例→  https://kaihatuiinkai.jp/time_shift/camera2.html

「1秒前の過去の画像」が表示されます。インターバル処理の数値で、画像書き換えの時間を変更します。100にすると、&lt;video&gt;動画と同様に見えます。

```camera2.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Camera2</title>
  <style>
  canvas, video{ border: 1px solid gray; }
  </style>
</head>
<body>
<h1>映像要素の画像を&lt;canvas&gt;に表示</h1>
1秒のインターバル処理でcanvasに表示<br>
video　　　　　　　　　　　　　　　　　canvas1<br>
<video  id="video"   width="300" height="200"></video>
<canvas id="canvas1" width="300" height="200"></canvas>
<script>
window.onload = () => {
  const video   = document.querySelector("#video");
  const canvas1 = document.querySelector("#canvas1");
  // カメラ設定  //
  const constraints = {
    audio: false,
    video: {
      width:  300,
      height: 200,
      facingMode: "user"   // フロントカメラを利用する
      // facingMode: { exact: "environment" }  // リアカメラを利用する場合
    }
  };
  // カメラを<video>と同期 //
  navigator.mediaDevices.getUserMedia(constraints)
  .then( (stream) => {
    video.srcObject = stream;
    video.onloadedmetadata = (e) => {
      video.play();
    };
  })
  .catch( (err) => {
    console.log(err.name + ": " + err.message);
  });
  // インターバル処理
  var intervalID = setInterval(function(){
    // canvas の準備
    picture = canvas1.getContext("2d");
    // video画像を canvas1に貼り付ける
    picture.drawImage(video, 0, 0, canvas1.width, canvas1.height);
  }, 1000);
};
</script>
</body>
</html>
```

#&lt;canvas1&gt;の画像を&lt;canvas2&gt;に表示
&lt;canvas1&gt;の画像を変数に入れて、その変数で&lt;canvas2&gt;に画像を表示します。
作動例→  https://kaihatuiinkai.jp/time_shift/camera3.html

```camera3.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Camera3</title>
  <style>
  canvas, video{ border: 1px solid gray; }
  </style>
</head>
<body>
<h2>映像要素の画像を&lt;canvas1&gt;に表示、&lt;canvas1&gt;の画像を&lt;canvas2&gt;に表示</h2>
video　　　　　　　　　　　　　　　　　canvas1　　　　　　　　　　　　　　　　　canvas2<br>
<video  id="video"   width="300" height="200"></video>
<canvas id="canvas1" width="300" height="200"></canvas>
<canvas id="canvas2" width="300" height="200"></canvas>
<script>
window.onload = () => {
  const video   = document.querySelector("#video");
  const canvas1 = document.querySelector("#canvas1");
  const canvas2 = document.querySelector("#canvas2");
  // カメラ設定  //
  const constraints = {
    audio: false,
    video: {
      width:  300,
      height: 200,
      facingMode: "user"   // フロントカメラを利用する
      // facingMode: { exact: "environment" }  // リアカメラを利用する場合
    }
  };
  // カメラを<video>と同期 //
  navigator.mediaDevices.getUserMedia(constraints)
  .then( (stream) => {
    video.srcObject = stream;
    video.onloadedmetadata = (e) => {
      video.play();
    };
  })
  .catch( (err) => {
    console.log(err.name + ": " + err.message);
  });
  // インターバル処理
  var intervalID = setInterval(function(){
    // canvas1 の準備
    picture = canvas1.getContext("2d");
    // canvas1に画像を貼り付ける
    picture.drawImage(video, 0, 0, canvas1.width, canvas1.height);
    // canvas1から画像を取得して変数に入れる
    var image = picture.getImageData(0, 0, canvas1.width, canvas1.height);
    // canvas2 に変数の画像を貼り付ける
    document.getElementById("canvas2").getContext('2d').putImageData(image, 0, 0);
  }, 1000);
};
</script>
</body>
</html>
```


#&lt;canvas1&gt;の画像を配列に入れて、&lt;canvas2&gt;に遅延表示
　配列変数を使って、&lt;canvas2&gt;に遅延表示します。初めは配列に画像がないため起動時にエラーが出るので、これを回避処理します。
作動例→  https://kaihatuiinkai.jp/time_shift/camera4.html

```camera4.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Time Shift Camera</title>
  <style>
  canvas, video{ border: 1px solid gray; }
  </style>
</head>
<body>
<h2>&lt;canvas1&gt;の画像を配列に入れて、&lt;canvas2&gt;に遅延表示</h2>
video　　　　　　　　　　　　　　　　　canvas1　　　　　　　　　　　　　　　　　canvas2<br>
<video id="video" width="300" height="200"></video>
<canvas id="canvas1" width="300" height="200"></canvas>
<canvas id="canvas2" width="300" height="200"></canvas>
<script>
var imageMax = 110;   // 遅延最大時間　最大10秒
var imageArray = new Array(imageMax); // 画像保存用
var imageNumber = 0;  // 現在の画像番号
var imageDisplay = 0; // 表示番号
var imageDelay = 10;  // 表示遅延  1秒
window.onload = () => {
  const video  = document.querySelector("#video");
  const canvas1 = document.querySelector("#canvas1");
  const canvas2 = document.querySelector("#canvas2");
  // カメラ設定  //
  const constraints = {
    audio: false,
    video: {
      width: 300,
      height: 200,
      facingMode: "user"   // フロントカメラを利用する
      // facingMode: { exact: "environment" }  // リアカメラを利用する場合
    }
  };
  // カメラを<video>と同期 //
  navigator.mediaDevices.getUserMedia(constraints)
  .then( (stream) => {
    video.srcObject = stream;
    video.onloadedmetadata = (e) => {
      video.play();
    };
  })
  .catch( (err) => {
    console.log(err.name + ": " + err.message);
  });

    // canvas1 の準備
    picture = canvas1.getContext("2d");
    // canvas1に画像を貼り付ける
    picture.drawImage(video, 0, 0, canvas1.width, canvas1.height);

  // インターバル処理
  var intervalID = setInterval(function(){
    // canvas1 の準備
    picture = canvas1.getContext("2d");
    // canvas1に画像を貼り付ける
    picture.drawImage(video, 0, 0, canvas1.width, canvas1.height);
    // 画像記録番号
    imageNumber = imageNumber + 1;
    if(imageNumber >= imageMax){
      imageNumber =0;
    };
    // canvas1から画像を取得して変数に入れる
    imageArray[imageNumber]= picture.getImageData(0, 0, canvas1.width, canvas1.height);
    // 遅延画像番号
    imageDisplay = imageNumber - imageDelay;
    if(imageDisplay <0 ){
      imageDisplay = imageDisplay + imageMax; // マイナスの処理
    }
    // エラー回避処理
    try {
    // canvas2 に変数の画像を貼り付ける
      document.getElementById("canvas2").getContext('2d').putImageData(imageArray[imageDisplay], 0, 0);
    }catch(e){
      // エラー時には何もしない
    };
  }, 100);
};
</script>
</body>
</html>
```

#iPhoneエラー対応
　Windowsのchrome、Macのsafari、iPadのsafariで動きます。でも、iPhoneで作動しません。対応策として

```
  const video  = document.querySelector("#video");
  video.setAttribute('autoplay', '');
  video.setAttribute('muted', '');
  video.setAttribute('playsinline', '');
```

としてください。
参照： iPhone の場合  video が止まってしまうことへの対処
https://otiai10.hatenablog.com/entry/2019/11/11/090124

　30秒間 × 10画像 = 300画像　を配列に入れると、600～800MBのメモリーを消費します。どこまで配列を大きくできるか？　挑戦してみてください。
　ブラウザで画像処理ができること、プログラムが短いことに技術の進歩を感じます。