みかづきブログ・カスタム

基本的にはちょちょいのほいです。

Express + socket.ioでiPhoneのカメラが捉えている映像をPCでリアルタイムプレビューするウェブサイトをつくる 🎥

前提

  1. 自前で証明書を発行済みでhttpsでlocalhostにアクセスできる
  2. iPhoneとPCは同一WiFiにつながっている
  3. iPhoneは1台、PCも1台
  4. iPhoneは直接見れない位置にある

という条件で、iPhoneのカメラが捉えている映像を確認したく、PCでプレビューできるようにしてみました。

仕組み

本当はWebRTCとかで、サーバを介さずに、iPhoneとPCがやりとりできるのがベストだと思うのですが、今回は手っ取り早く実装するために、socket.ioを使って通信します。

  1. iPhone側のウェブサイト 👉 カメラが捉えている映像をbase64に変換したものをサーバに送り続ける
  2. サーバ 👉 iPhoneのブラウザから送られてくる文字列をPCのブラウザに送る
  3. PC側のウェブサイト 👉 サーバから受け取ったbase64を画像としてレンダリングする

という流れです。




camera.html(iPhone側のページ)

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8" />
  <title>camera</title>
  <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no, shrink-to-fit=no" />
  <style>
    body {
      margin: 0; padding: 0;
    }

    video {
      width: 100%; height: 100%;
    }
  </style>
  <script src="./socket.io.min.js"></script>
</head>
<body>
  <video id="video" autoplay playsinline muted></video>
  <script>
    navigator.mediaDevices.getUserMedia({
      audio: false,
      video: {
        facingMode: {
          exact: 'environment' // リアカメラを使用
        }
      }
    }).then((stream) => {
      const FPS = 8;
      const socket = io.connect();
      const video = document.getElementById('video');
      const canvas = document.createElement('canvas');
      const ctx = canvas.getContext('2d');

      video.oncanplay = () => {
        // 通信量を抑えるためにサイズを小さくする
        const width = video.clientWidth / 2;
        const height = video.clientHeight / 2;

        tick();

        function tick() {
          const begin = Date.now();

          canvas.width = width;
          canvas.height = height;

          ctx.drawImage(video, 0, 0, width, height);

          const dataURL = canvas.toDataURL('image/jpeg');

          // サーバにbase64を送る
          socket.emit('video', {
            dataURL
          });

          const delay = 1000 / FPS - (Date.now() - begin);

          // 8FPSで動かす
          setTimeout(tick, delay);
        }
      };

      video.srcObject = stream;
    }).catch((err) => alert(err));
  </script>
</body>
</html>

app.js(サーバ)

const https = require('https');
const path = require('path');
const fs = require('fs');
const express = require('express');
const app = require('express')();
// httpsじゃないとカメラを開放できないので、事前に「server.key」「server.crt」を作っておく
// https://blog.kimizuka.org/entry/2022/12/26/234957
const options = {
  key: fs.readFileSync(path.resolve(__dirname, './server.key')),
  cert: fs.readFileSync(path.resolve(__dirname, './server.crt'))
};

app.use('/', express.static(`${__dirname}/public`));

const server = https.createServer(options, app).listen(3000);
const io = require('socket.io')(server);

io.on('connection', (socket) => {
  socket.on('video', (dataURL) => {
    // iPhoneのブラウザから送られてきた文字列をPCのブラウザに送る
    io.emit('video', dataURL);
  });
});

index.html(PC側のページ)

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8" />
  <title>tv</title>
  <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no, shrink-to-fit=no" />
  <style>
    body {
      margin: 0; padding: 0;
    }
  </style>
  <script src="./socket.io.min.js"></script>
</head>
<body>
  <img id="img" />
  <script>
    const socket = io.connect();
    const img = document.getElementById('img');

    socket.on('video', ({ dataURL }) => {
      img.src = dataURL; // サーバから送られてきたbase64をimageタグのsrcに設定する
    });
  </script>
</body>
</html>

これで、iPhoneのカメラが捉えている映像をPCでリアルタイムプレビューするウェブサイトができました。

camera.htmlには、ローカルIPでアクセスする必要があります。
僕は、こういうときのためにMacのメニューバーに常にローカルIPを表示しています。
blog.kimizuka.org