Konubinix' opinionated web of thoughts

Eagle Animation With Webrtc Camera

Fleeting

eagle animation with webrtc camera

Following the idea from aiortc to create a remote web camera with an android phone, to continue the idea of stopmotion android apps.

  • peerjs to sync them with a private server,
  • the camera registers itself to the name “camera”,
  • the stopmotion app registers itself to the name “control”,
  • when connected, the camera calls the stopmotion app,

code in the camera

I used the exact same code as the one used in aiortc to create a remote web camera with an android phone.

<html>
  <head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script defer src="https://ipfs.konubinix.eu/p/bafkreic33rowgvvugzgzajagkuwidfnit2dyqyn465iygfs67agsukk24i?orig=https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
    <script src="https://ipfs.konubinix.eu/p/bafybeihp5kzlgqt56dmy5l4z7kpymfc4kn3fnehrrtr7cid7cn7ra36yha?orig=https://cdn.tailwindcss.com/3.4.3"></script>
    <script src="https://unpkg.com/peerjs@1.5.2/dist/peerjs.min.js"></script>
    <script>
      document.addEventListener('alpine:init', () => {
          Alpine.data('app', () => ({
              pc: null,
              message: "",
              video: null,
              dc: null,
              peer: null,
              conn: null,
              peerId: null,
              current_call: null,
              wakelock: null,
              width: {min: 320, max: 5000, value: 640},
              currentwidth: null,
              currentFrameRate: null,
              configure: true,
      bodyClass: "",
      async init () {
      this.video = document.getElementById("video")
      this.peer = new Peer("camera", {host: "home", port: 9999, path: "/peerjs/"});
      this.peer.on("error", (err) => {
        this.message += ` error: ${err}`
        });
        this.peer.on('open', (id) => {
        this.message += `connected to peerjs with id ${id}`
        this.peerId = id
        });
        if(! 'mediaDevices' in navigator) {
        alert( 'Your browser does not support media devices.' );
        }
        await this.setupMediaStream()
        this.peer.on('connection', async (conn) => {
        this.message += "connected to the controller"
        this.conn = conn
        this.message += "calling the controller"
        this.current_call = this.peer.call("control", this.mediaStream)
        this.conn.on("open", async () => {
        conn.send(JSON.stringify({type: "configure", value: this.pc === null ? true : false}))
        })
        conn.on('data', async (data) => {
        this.message += `got ${data}`
        const order = JSON.parse(data)
        if(order.action === "start") {
        await this.start(this.width)
        conn.send(JSON.stringify({"type": "answer", "message": "started"}))
        }
        else if(order.action === "stop") {
        await this.stop()
        conn.send(JSON.stringify({"type": "answer", "message": "stopped"}))
        }
        else if(order.action === "setupMediaStream")
        {
        this.width.value = order.width
        await this.setupMediaStream()
        conn.send(JSON.stringify({"type": "answer", "message": "mediaStreamUpdate"}))
        }
        else {
        this.message("Unrecognized")
        }
        });
        });
        try{
        navigator.wakeLock.request("screen").then(
        (w) => {
        this.wakelock = w;
        this.message += "wakelock acquired"
        }
        ).catch((err) => {
        this.message += `${err.name}, ${err.message}`;
        });
        } catch (err) {
        this.message += err.toString()
        };
        },

        createPeerConnection() {
        this.pc = new RTCPeerConnection();
        this.pc.addEventListener('track', (evt) => {
        this.video.srcObject = evt.streams[0];
        });
        },

        async setupMediaStream() {
        this.mediaStream = await navigator.mediaDevices.getUserMedia(
        {
        video: {
        width: { ideal: this.width.value },
        height: { ideal: this.width.value },
        frameRate: 30,
        facingMode: {exact: "environment"},
        },
        audio: false,
        }
        );
        // window.ms = this.mediaStream
        this.currentwidth = this.mediaStream.getVideoTracks()[0].getSettings().width
        this.currentFrameRate = this.mediaStream.getVideoTracks()[0].getSettings().frameRate
        this.video.srcObject = this.mediaStream
        if(this.conn !== null){
        this.current_call.close()
        this.current_call = this.peer.call("control", this.mediaStream)
        }
        },

        async start() {
        this.configure = false
        this.bodyClass = "text-slate-700 bg-black"
        this.createPeerConnection()
        this.mediaStream.getTracks().forEach((track) => {
        this.pc.addTrack(track, this.mediaStream);
        });
        await this.negociate()
        },
        async negociate() {
        this.message += "\nneg start"
        var offer = await this.pc.createOffer();
        await this.pc.setLocalDescription(offer);
        try{
        var resp = await fetch('http://192.168.1.245:9999/offer', {
        body: JSON.stringify({
        sdp: offer.sdp,
        type: offer.type,
        }),
        headers: {
        'Content-Type': "application/json"
        },
        method: "POST"
        });
        } catch (err) {
        alert(err.toString())
        alert(JSON.stringify(err));
        throw err
        };
        this.message += "\nneg sent"

        var answer = await resp.json()
        await this.pc.setRemoteDescription(answer);
        this.message += "\nconnected"

        },
        async stop() {
        // if (this.wakelock != null) {
        //     this.wakelock.release().then(() => {
        //         this.wakelock = null;
        //         // alert("released");
        //     }).catch((err) => {
        //         alert(`${err.name}, ${err.message}`);
        //     });
        // }
        // this.current_call.close()
        this.pc.getTransceivers().forEach(function(transceiver) {
        if (transceiver.stop) {
        transceiver.stop();
        }
        });
        this.pc.getSenders().forEach(function(sender) {
        sender.track.stop();
        });

        // close peer connection
        setTimeout(async () => {
        this.pc.close();
        this.pc = null
        this.message += "\nall closed"
        this.conn.send(JSON.stringify({"type": "answer", "message": "allclosed"}))
        this.bodyClass = ""
        this.configure = true
        setTimeout(async () => {
        this.message += "connecting back"
        await this.setupMediaStream()
        this.current_call = this.peer.call("control", this.mediaStream)
        }, 500)
        }, 500);

        },
        }))
        })
    </script>
  </head>
  <body x-data="app" :class="bodyClass">
    <div>
      <!-- <button @click="start">Start</button> -->
      <!-- <button @click="stop">Stop</button> -->
      <div x-text="peerId"></div>
      <div x-text="message"></div>
      <div x-show="configure">
        <span x-text="width.value"></span>
        <span x-text="currentwidth"></span>
        <span x-text="currentFrameRate"></span>
        <div class="justify-center sm:px-20 p-15 h-screen" x-data="{show: true}">
          <button class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800" @click="show = !show ; $refs.video.scrollIntoView()" x-text="show ? 'Hide' : 'Show'"></button>
          <video id="video" @dblclick="$event.target.requestFullscreen()" x-show="show" class="object-scale-down max-h-full" x-ref="video" poster="https://d1tobl1u8ox4qn.cloudfront.net/2018/05/3814a83b-a2d4-4dfd-8945-f1cd003eb16f-1920x1080.jpg" autoplay="true" playsinline="true"></video>
        </div>
      </div>
    </div>
  </body>
</html>

code in eagle animations

I forked eagle-animation and added a camera called webrtc, copy of the webcam one, using the stream got from peerjs instead of one got from navigator.mediaDevices.getUserMedia.

See in: https://github.com/Konubinix/eagle-animation

how to run it

  1. first, run peerjs
  2. then, run the camera
  3. then, run the control

If the control cannot connect to the camera, you have to start the camera and reload the control page

This is a snapshot, generated with

set -eu
# npm i --force
npm run build:web

ipfa out/web

https://ipfs.konubinix.eu/p/bafybeif3g6ux2r7k7b3mzkdufuqwb4fynsivfu2hwkxz2yhtvashgxoipm?filename_out_web.nil

config

Notes linking here