$ cd /home/Lu/

Keep-learning Lu

17 May 2020

Display MJPEG stream as video in Javascript and .ejs

With the development of the society, cameras are everywhere. Before we start this “.mjpeg to video” tutorial, I have to emphasise the importance of the security of your cameras. Personally, I put stickers on cameras of every PC of mine, and aware of my familly camera from Xiaomi (If I had paid attention to the security of cameras, I would make my own, rather than purchasing a merchandise).

Agenda

  1. Background
  2. Javascript Solution
  3. Ejs Solution

1. Background

The background of this demand is that my M2 project group and I are developing a facial recognition project as our master's final project. Regarding the current confinement situation of Europe, we are not able to deploy it in our campus. The professor suggested us to use the exsiting camera from some IP camera live website, such as Insecam. As you can see from the screenshot below, it includes plenty of countries and cameras.

2020-05-17-15-59-45.png

By copying the URL of the image (right click), we can easily get the URL:

2020-05-17-16-07-03.png

2. Javascript Solution

I will take this “video” as an example. As we all know, if try to add a video into HTML, we can simply add <video> tag to realise, but it won't apply to this kind of “video”. Considering the storage and power consumption, surveillance videos are just serials of pictures playing frequently.

By this means, we can program the Javascript:

// namespace MJPEG { ...
var MJPEG = (function(module) {
  "use strict";

  // class Stream { ...
  module.Stream = function(args) {
    var self = this;
    var autoStart = args.autoStart || false;

    self.url = args.url;
    self.refreshRate = args.refreshRate || 500;
    self.onStart = args.onStart || null;
    self.onFrame = args.onFrame || null;
    self.onStop = args.onStop || null;
    self.callbacks = {};
    self.running = false;
    self.frameTimer = 0;

    self.img = new Image();
    if (autoStart) {
      self.img.onload = self.start;
    }
    self.img.src = self.url;

    function setRunning(running) {
      self.running = running;
      if (self.running) {
        self.img.src = self.url;
        self.frameTimer = setInterval(function() {
          if (self.onFrame) {
            self.onFrame(self.img);
          }
        }, self.refreshRate);
        if (self.onStart) {
          self.onStart();
        }
      } else {
        self.img.src = "#";
        clearInterval(self.frameTimer);
        if (self.onStop) {
          self.onStop();
        }
      }
    }

    self.start = function() { setRunning(true); }
    self.stop = function() { setRunning(false); }
  };

  // class Player { ...
  module.Player = function(canvas, url, options) {

    var self = this;
    if (typeof canvas === "string" || canvas instanceof String) {
      canvas = document.getElementById(canvas);
    }
    var context = canvas.getContext("2d");

    if (! options) {
      options = {};
    }
    options.url = url;
    options.onFrame = updateFrame;
    options.onStart = function() { console.log("started"); }
    options.onStop = function() { console.log("stopped"); }

    self.stream = new module.Stream(options);

    canvas.addEventListener("click", function() {
      if (self.stream.running) { self.stop(); }
      else { self.start(); }
    }, false);

    function scaleRect(srcSize, dstSize) {
      var ratio = Math.min(dstSize.width / srcSize.width,
                           dstSize.height / srcSize.height);
      var newRect = {
        x: 0, y: 0,
        width: srcSize.width * ratio,
        height: srcSize.height * ratio
      };
      newRect.x = (dstSize.width/2) - (newRect.width/2);
      newRect.y = (dstSize.height/2) - (newRect.height/2);
      return newRect;
    }

    function updateFrame(img) {
        var srcRect = {
          x: 0, y: 0,
          width: img.naturalWidth,
          height: img.naturalHeight
        };
        var dstRect = scaleRect(srcRect, {
          width: canvas.width,
          height: canvas.height
        });
      try {
        context.drawImage(img,
          srcRect.x,
          srcRect.y,
          srcRect.width,
          srcRect.height,
          dstRect.x,
          dstRect.y,
          dstRect.width,
          dstRect.height
        );
        console.log(".");
      } catch (e) {
        // if we can't draw, don't bother updating anymore
        self.stop();
        console.log("!");
        throw e;
      }
    }

    self.start = function() { self.stream.start(); }
    self.stop = function() { self.stream.stop(); }
  };

  return module;

})(MJPEG || {});

And the complimentary HTML:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8"/>
    <title>Player</title>
  </head>
  <body>
    <canvas id="player" style="background: #000;">
      Your browser sucks.
    </canvas>
  </body>
  <script src="mjpeg.js"></script>
  <script>
    //Leave your .mjpeg video URL here.
    var player = new MJPEG.Player("player", "http://2.10.128.144:81/mjpg/video.mjpg");
    player.start();
  </script>
</html>

We defined a player function, which updates the frames. To desplay every picture/frame continually, we have to use <canvas> tag drawing a canvas literally, neither <img> nor <video>.

Two examples of <img> and <video> tags in HTML:

<img src="smiley.gif" alt="Smiley face" height="42" width="42">
<video width="320" height="240" controls>
  <source src="movie.mp4" type="video/mp4">
  <source src="movie.ogg" type="video/ogg">
  Your browser does not support the video tag.
</video>

And the example of <canvas>:

<canvas id="myCanvas">
Your browser does not support the canvas tag.
</canvas>

<script>
var canvas = document.getElementById("myCanvas");
var ctx = canvas.getContext("2d");
ctx.fillStyle = "#FF0000";
ctx.fillRect(0, 0, 80, 80);
</script>

The <canvas> tag is used to draw graphics, on the fly, via scripting (usually JavaScript).

The <canvas> tag is transparent, and is only a container for graphics, you must use a script to actually draw the graphics.

Any text inside the <canvas> element will be displayed in browsers with JavaScript disabled and in browsers that do not support <canvas>.

The result of our codes (the point “.” in console means it works, it will output “!"):

2020-05-17-18-15-08.png

3. Ejs Solution

However, how can we apply it to .ejs (express framework)? Quite simple:

2020-05-17-17-58-41.png

There is a mjpeg.ejs inside /views/includes, where we place all the scripts, and another streaming.ejs exists in /views where we place different page views. In this case, the stream will display on the streaming page, so I simply add a line (with red rectangle showing in the screenshot above) to include mjpeg script .

<!doctype html>
<html lang="en">

<head>
    <title>Face Server - Streaming</title>

    <%- include('includes/head'); %>
</head>

<body>

    <%- include('includes/navbar'); %>

    <h2 class="mb-4">Bienvenue sur FaceServer !</h2>
    <p>Vous pourrez via cet interface observer la vue de la caméra ainsi que constater les logs de détections et de reconnaissances des personnes qui passent dans son champ. </p>
  	<!-- include mjpeg script -->
    <%- include ('includes/mjpeg') %>
    <%- include ('includes/footer') %>
    <%- include ('includes/scripts') %>

</body>

</html>

Okay, so we've already told the page to load the script, now we need to convert the tranditional HTML/Javascript into ejs. Basically, we just copy-paste the scripts and put them together like this:

<canvas id="player" style="width:100%;">
    Your browser sucks.
</canvas>
<script>
    // namespace MJPEG { ...
        var MJPEG = (function(module) {
          "use strict";
        
          // class Stream { ...
          module.Stream = function(args) {
            var self = this;
            var autoStart = args.autoStart || false;
        
            self.url = args.url;
            self.refreshRate = args.refreshRate || 500;
            self.onStart = args.onStart || null;
            self.onFrame = args.onFrame || null;
            self.onStop = args.onStop || null;
            self.callbacks = {};
            self.running = false;
            self.frameTimer = 0;
        
            self.img = new Image();
            if (autoStart) {
              self.img.onload = self.start;
            }
            self.img.src = self.url;
        
            function setRunning(running) {
              self.running = running;
              if (self.running) {
                self.img.src = self.url;
                self.frameTimer = setInterval(function() {
                  if (self.onFrame) {
                    self.onFrame(self.img);
                  }
                }, self.refreshRate);
                if (self.onStart) {
                  self.onStart();
                }
              } else {
                self.img.src = "#";
                clearInterval(self.frameTimer);
                if (self.onStop) {
                  self.onStop();
                }
              }
            }
        
            self.start = function() { setRunning(true); }
            self.stop = function() { setRunning(false); }
          };
        
          // class Player { ...
          module.Player = function(canvas, url, options) {
        
            var self = this;
            if (typeof canvas === "string" || canvas instanceof String) {
              canvas = document.getElementById(canvas);
            }
            var context = canvas.getContext("2d");
        
            if (! options) {
              options = {};
            }
            options.url = url;
            options.onFrame = updateFrame;
            options.onStart = function() { console.log("started"); }
            options.onStop = function() { console.log("stopped"); }
        
            self.stream = new module.Stream(options);
        
            canvas.addEventListener("click", function() {
              if (self.stream.running) { self.stop(); }
              else { self.start(); }
            }, false);
        
            function scaleRect(srcSize, dstSize) {
              var ratio = Math.min(dstSize.width / srcSize.width,
                                   dstSize.height / srcSize.height);
              var newRect = {
                x: 0, y: 0,
                width: srcSize.width * ratio,
                height: srcSize.height * ratio
              };
              newRect.x = (dstSize.width/2) - (newRect.width/2);
              newRect.y = (dstSize.height/2) - (newRect.height/2);
              return newRect;
            }
        
            function updateFrame(img) {
                var srcRect = {
                  x: 0, y: 0,
                  width: img.naturalWidth,
                  height: img.naturalHeight
                };
                var dstRect = scaleRect(srcRect, {
                  width: canvas.width,
                  height: canvas.height
                });
              try {
                context.drawImage(img,
                  srcRect.x,
                  srcRect.y,
                  srcRect.width,
                  srcRect.height,
                  dstRect.x,
                  dstRect.y,
                  dstRect.width,
                  dstRect.height
                );
                console.log(".");
              } catch (e) {
                // if we can't draw, don't bother updating anymore
                self.stop();
                console.log("!");
                throw e;
              }
            }
        
            self.start = function() { self.stream.start(); }
            self.stop = function() { self.stream.stop(); }
          };
        
          return module;
        
        })(MJPEG || {});
    </script>
<script>
  var player = new MJPEG.Player("player", "http://2.10.128.144:81/mjpg/video.mjpg");
  player.start();
</script>
<style>
    *{
        margin:0;
        padding:0;
    }
    #player{
        background:#000;
        display: block;
    }
</style>

Done! After npm start, we navigate to http://localhost:3000/streaming (depends on your port, etc.. ):

2020-05-17-18-19-20.png

comments powered by Disqus