themactep / thingino-firmware

Open-source firmware for Ingenic SoC IP cameras
https://thingino.com
MIT License
159 stars 50 forks source link

Realtime preview in WebUI #41

Closed themactep closed 2 months ago

themactep commented 6 months ago

JPEG preview sucks and so 90s. We need a streaming preview.

gtxaspec commented 3 months ago

can we do this with websockets? simulate mjpeg?

themactep commented 3 months ago
var img = new Image();
var ws = new WebSocket('ws://192.168.1.10:8080/');
ws.onmessage = function(event) {
     var urlObject = URL.createObjectURL(event.data);
     img.src = urlObject;
     preview.src = img;
}

we could try something like that

virmaior commented 2 months ago

does the webserver in thingino support websockets?

if not, here's a modified version of code I use for wz_mini (my setup is designed for multiple cameras behind a proxy but I've tried to modify it to work for a single camera here):

const cam_tool = {
  feed_interval_frequency : 3000,
  spacing:false,
  feed_interval : false,
  tick: 0,
  feeds : false,
  buttons: [],
  loads: [],
  load_sum :0,
  load_fails:0,
  jpeg_url:"http://192.168.1.10:8080/",
  run_mode: function(e) {
    e.preventDefault();
    e.stopPropagation();

      for (var i = 0; i < cam_tool.modes.length; i++) {
                cam_tool.modes[i].classList.remove('active_button');
        }

    e.target.classList.add('active_button');
    if (e.target.getAttribute("mode") == 'auto') {  
            var average = cam_tool.load_sum / cam_tool.loads.length;
            var use =  parseInt(average * 2 / 10) * 10;
            cam_tool.feed_interval_frequency = use;
            cam_tool.calc_spacing();    
    }
    if (e.target.getAttribute("mode") == 'manual') {
        cam_tool.feed_interval_frequency = document.querySelector("DIV.load_mode[mode='manual'] INPUT").value;
                cam_tool.calc_spacing();
    }
  },

  init : function(feed_interval_frequency) {
    this.feed_interval_frequency = feed_interval_frequency;

    cam_tool.buttons["start"] = document.querySelector("BUTTON[action='start']");
    cam_tool.buttons["pause"] = document.querySelector("BUTTON[action='pause']");

    cam_tool.buttons["start"].addEventListener('click',cam_tool.start);
    cam_tool.buttons["pause"].addEventListener('click',cam_tool.pause);

    cam_tool.modes = document.getElementsByClassName('load_mode');
    for (var i = 0; i < cam_tool.modes.length; i++) {
            cam_tool.modes[i].addEventListener('click', cam_tool.run_mode);
    }

    document.querySelector("DIV.load_mode[mode='manual'] INPUT").value = cam_tool.feed_interval_frequency;
    cam_tool.calc_spacing();
    cam_tool.start();

  },
  log:function(message) {
    console.log("camera " + message);
  },
  tick: function() {
        var current_tick = cam_tool.tick * 10; //correct for 10ms interval
        var target_tick = cam_tool.spacing;
        if (((current_tick)  % cam_tool.feed_interval_frequency) == (target_tick ))  { 
            cam_tool.update();
        } 
    cam_tool.tick++;
  },
  clear(cam)
  {
    cam.setAttribute('current','n');
    cam.classList.remove('broken_cam');
  },
  update()
  {
          var cam1 = document.querySelector('img.cam_img[f=1]');
          var cam2 = document.querySelector('img.cam_img[f=2]');
          var cam = cam1;

          if (cam1.getAttribute('current') == "y") {
                  cam_tool.clear(cam1);
                  var cam = cam2;
          } else if (cam2.getAttribute('current') == "y") {
                cam_tool.clear(cam2);
          }
          else {
                  cam1.classList.add('broken_cam');
                  cam_tool.notify(i,'neither is loaded on camera ');
                  return false;
          }
          var load_start = new Date().getTime();
          cam.setAttribute('load_start',load_start);
          cam.src = cam.src.split("&")[0] + "&load=" + load_start;
  },
  update_freq(event) {
    console.log('set frequency to ' + event.target.value);
    cam_tool.feed_interval_frequency = event.target.value;  
    cam_tool.calc_spacing();
  },
 set_buttons:function(active)
 {
    for (let key in cam_tool.buttons) {
            cam_tool.buttons[key].classList.remove('active_button');
            cam_tool.buttons[key].classList.add('inactive_button');
    }
    cam_tool.buttons[active].classList.add("active_button");
    cam_tool.buttons[active].classList.remove("inactive_button");
},
  pause: function() {
    cam_tool.set_buttons('pause');
    clearInterval(cam_tool.feed_interval);
    cam_tool.tick = 0;
    releaseWakeState();
  },
  start : function() {
    if (cam_tool.feed_interval) { cam_tool.pause(); }
        cam_tool.set_buttons('start');
    cam_tool.feed_interval = setInterval(cam_tool.update, 10);
    requestWakeLock();
  }

}

if it's in auto-mode, it will be adjusting the interval based on how long it's taking to get responses.

running on my home intranet within the limitations of what wz_mini can do (i.e. what iCamera allows), I can do about 6 frames / second on 5 or 6 cameras at once.

gtxaspec commented 2 months ago

we use httpd from busybox, like in wz_mini, but the new version of our streamer supports websockets natively... =D

gtxaspec commented 2 months ago

new preview is live in latest builds

themactep commented 2 months ago

I think the issue is fulfilled, now we can perfect it further.