Reload MJPEG Camera streams until they are shown on dashboard

Unfortunately picture entity cards often start showing no stream but the broken image icon.

If I refresh dashboard for up to 10 times it shows the streams as expected

Here is the dashboard code:

views:
  - title: Kameras
    type: custom:grid-layout
    layout:
      grid-template-columns: 50% 50%
      grid-template-rows: auto auto
      grid-template-areas: |
        "a b"
        "c d"
    cards:
      - type: vertical-stack
        view_layout:
          grid-area: a
        cards:
          - show_state: false
            show_name: false
            camera_view: live
            type: picture-entity
            entity: camera.cam1
            tap_action:
              action: none
            hold_action:
              action: none
          - type: entities
            entities:
              - entity: binary_sensor.blue_iris_kamera_01_eingang
                type: custom:template-entity-row
                name: Person
                state: '{{ "Erkannt" if is_state(config.entity, "on") else "Keine" }}'
                icon: >-
                  {{ "mdi:motion-sensor" if is_state(config.entity, "on") else
                  "mdi:motion-sensor-off" }}
      - type: vertical-stack
        view_layout:
          grid-area: b
        cards:
          - show_state: false
            show_name: false
            camera_view: live
            type: picture-entity
            entity: camera.cam2
            tap_action:
              action: none
            hold_action:
              action: none
          - type: entities
            entities:
              - entity: binary_sensor.blue_iris_kamera_02_terrasse
                type: custom:template-entity-row
                name: Person
                state: '{{ "Erkannt" if is_state(config.entity, "on") else "Keine" }}'
                icon: >-
                  {{ "mdi:motion-sensor" if is_state(config.entity, "on") else
                  "mdi:motion-sensor-off" }}
      - type: vertical-stack
        view_layout:
          grid-area: c
        cards:
          - show_state: false
            show_name: false
            camera_view: live
            type: picture-entity
            entity: camera.cam3
            tap_action:
              action: none
            hold_action:
              action: none
          - type: entities
            entities:
              - entity: binary_sensor.blue_iris_kamera_03_garten
                type: custom:template-entity-row
                name: Person
                state: '{{ "Erkannt" if is_state(config.entity, "on") else "Keine" }}'
                icon: >-
                  {{ "mdi:motion-sensor" if is_state(config.entity, "on") else
                  "mdi:motion-sensor-off" }}
type: masonry

So my idea is to auto refresh dashboard or even better reload images up to 10 times until the streams are visible.

Made some investigations and came up with a solution from Dumitru Glavan that can be inspected here: https://github.com/doomhz/jQuery-Image-Reloader/tree/master

After modifying the code for Home Assistant compatibility, I just need to add this HTML code in the <body></body> section of the dashboard:

    <link href="https://ha.hoehn.nl/local/image_reloader.css" rel="stylesheet" type="text/css" media="screen" />
    <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
    <script type="text/javascript" src="https://ha.hoehn.nl/local/jquery.imageReloader.js"></script>
    <script type="text/javascript">
      if (window.jQuery) {
        $("img").addClass("slow-images");
        $(".slow-images").imageReloader();
      } else console.log("jQuery not available!");
    </script>

Please try out this full HTML sample page:

<html>
  <head>
    <title>jQuery Image Reloader</title>
  </head>
  <body>
    <h1>jQuery Image Reloader</h1>

    <p>Below are two images. The first one is non-existent. The browser tries to load them both, but it fails. The Image Reloader plugin retries to load the non-existent image 10 times by default. In case that the image could not be loaded, it displays the browser's default broken image with an alt attribute.</p>

    <img src="http://dumitruglavan.com/non_existent_image.jpg" alt="Image not loaded!" width="155" height="200">
    <img src="https://ha.hoehn.nl/local/Portrait_Manfred_Hoehn_2022.jpg" width="155" height="200">
    
    <p><br>This plugin is effective when you load images from a cloud (i.e. Amazon S3) and the CDN has a delay until the image is accessible. The browser will display a broken image icon instead and won't retry to reload it. jQuery Image Reloader will take care of that.</p>

    <h2>Plugin code</h2>

    <p>The only code you need to activate the plugin is this:</p>

    <code>
      $(".slow-images").imageReloader();
    </code>

    <h2>Options:</h2>

    <dl>
      <dt><b>loadingClass</b>: "loading-image"</dt>
      <dd>
        The plugin hides the original image until it's successfully loaded and replaces it with a DIV that will take this CSS class.
      </dd>
      <dt><b>reloadTime</b>: 1500</dt>
      <dd>
        Set up the time between the reload attempts.
      </dd>
      <dt><b>maxTries</b>: 10</dt>
      <dd>
        Set up how many reload attempts should be made until the broken icon is displayed.
      </dd>
    </dl>

    <link href="https://ha.hoehn.nl/local/image_reloader.css" rel="stylesheet" type="text/css" media="screen" />
    <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
    <script type="text/javascript" src="https://ha.hoehn.nl/local/jquery.imageReloader.js"></script>
    <script type="text/javascript">
      if (window.jQuery) {
        $("img").addClass("slow-images");
        $(".slow-images").imageReloader();
      } else console.log("jQuery not available!");
    </script>      
  </body>
</html>

How can this be achieved elegantly?
As far as I read documentation and community content themes/skins could be a suitable route to go.

What I couldn’t find is how and from where I can copy the default theme and how exactly to insert body HTML code.

Another question is if themes and skins are synonyms?

Second approach I figured out is to have custom JavaScript code in Home Assistant but I don’t know how to determine when to inject HTML code and how to hook with DOM parsing phases.

A third approach could be to create a custom card or custom picture entity card to inject HTML body code.

What is your experience or opinion and what would be the best way to go?

Short update:

I decided to create a JavaScript code and call it using configuration.yaml:

frontend:
  themes: !include_dir_merge_named themes
  extra_module_url:
    - /local/dist/ha.imageReloader.js

This JavaScript uses module GitHub - elchininet/home-assistant-query-selector: Easily query Home Assistant DOM elements in an asynchronous way to hook into event listener for dashboard DOM with various shadow root sections.

As a newbie to JavaScript it’s been a hard time to get the first line of the home-assistant-query-selector work.

import { HAQuerySelector } from 'home-assistant-query-selector';

For JavaScript to import other modules it’s necessary to compile a target .js file containing all module source files and your own JavaScript code.

I chose to use npm and parcel to do this. Usually programmer’s prefer webpack instead of parcel but parcel is easier to configure. This as a hint. Here is a bash script and the package.json file I use:

Put this package.json file before running the bash script under /config/www

{
  "name": "www",
  "version": "1.0.0",
  "description": "",
  "scripts": {
    "build": "parcel build"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "type": "commonjs",
  "dependencies": {
    "home-assistant-query-selector": "^4.3.0"
  },
  "devDependencies": {
    "parcel": "^2.14.4"
  },
  "source": "ha.imageReloader.js"
}
#!/bin/bash
# Get node.js, npm and parcel if not installed
npm -v &> /dev/null
if [ $? -ne 0 ];
then
    apk add --no-cache nodejs-current yarn
    yarn global add npm
    yarn cache clean
    apk del yarn
    npm install parcel
    npm install home-assistant-query-selector -prefix /config/www
    npm run build --prefix /config/www
fi

This is the JavaScript file /config/www/ha.imageReloader.js I developed so far but it’s still not working. Will have to debug and see how to get it running.

const html = `
  <link href="https://ha.hoehn.nl/local/image_reloader.css" rel="stylesheet" type="text/css" media="screen" />
  <!-- <script type="module" src="http://homeassistant.local:8123/local/node_modules/home-assistant-query-selector/dist/esm/index.js"></script> -->
  <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js" onload="jQueryLoaded()"></script>
  <script type="text/javascript">
    function jQueryLoaded() {
      (function ($) {
        $.fn.imageReloader = function (options) {

          options = $.extend({}, options, {
            loadingClass: "loading-image",
            reloadTime: 1500,
            maxTries: 10
          });

          var $self = $(this);

          if ($self.length > 1) {
            $self.each(function (i, el) {
              $(el).imageReloader(options);
            });
            return $self;
          }

          $self.data("reload-times", 0);
          var imageHeight = $self.height()
          var imageWidth  = $self.width()

          var $imageReplacer = $('<div class="' + options.loadingClass + '">');
          $imageReplacer.css({height: imageHeight, width: imageWidth})
          $imageReplacer.hide();
          $imageReplacer.insertAfter($self);

          var showImage = function () {
            $self.show();
            $imageReplacer.remove();
          };

          $self.bind("error", function () {
            $self.hide();
            $imageReplacer.show()
            var reloadTimes = $self.data("reload-times");
            if (reloadTimes < options.maxTries) {
              setTimeout(function () {
                $self.attr("src", $self.attr("src"));
                var reloadTimes = $self.data("reload-times");
                reloadTimes++;
                $self.data("reload-times", reloadTimes);
              }, options.reloadTime);
            } else if (!$self.is(":visible")) {
              showImage();
            }
          });

          $self.bind("load", function () {
            showImage();
          });

          return this;
        };

      })(jQuery);
    }
    
  </script>
`;

const fragment = document.createRange().createContextualFragment(html);
document.body.append(fragment);

import { HAQuerySelector } from 'home-assistant-query-selector';

const instance = new HAQuerySelector();

// This event will be triggered every time a lovelace dashboard loads
// You can also use the enum value HAQuerySelectorEvent.ON_LOVELACE_PANEL_LOAD
instance.addEventListener('onLovelacePanelLoad', ({ detail }) => {

  const { HA_PANEL_LOVELACE } = detail;

  // Querying ha-camera-stream shadowRoot
  //document.querySelector("body > home-assistant").shadowRoot.querySelector("home-assistant-main").shadowRoot.querySelector("ha-drawer > partial-panel-resolver > ha-panel-lovelace").shadowRoot.querySelector("hui-root").shadowRoot.querySelector("#view > hui-view")
  HA_PANEL_LOVELACE.selector.deepQuery('grid-layout').$.query('div').element
    .then((gridLayoutElement) => {
      console.log("GRID-LAYOUT", gridLayoutElement);
      // Get all img elements
      const imgElements = []
      function findImgElements(root) {
          for (const {shadowRoot} of root.querySelectorAll("*")) {
              if (shadowRoot) {
                  // Look for img elements in the current root
                  imgElements.push(...shadowRoot.querySelectorAll("img"));
                  // Look for more roots in the current root
                  findImgElements(shadowRoot);
              }
          }
      }
      findImgElements(gridLayoutElement);

      // Add slow-images class to img imgElements
      for (const imgElement of imgElements) {
        imgElement.className = "slow-images";
      }
      console.log("IMG imgElements", imgElements);

      $(".slow-images").imageReloader(); //This line is still not working!

    });

});

// Start to listen
instance.listen();

//console.log("imageReloader Script executed");

After changing this JavaScript file you have to update the parcel package with this command:

npm run build --prefix /config/www

Will keep you updated how it will go on with this project.

Hope the large community of JavaScript developers who use Home Assistant can help me a little :slight_smile:

So far it’s been a very hard and time consuming way taking a lot of days and nights to get at least until this point.

Any help or comment is highly appreciated.

I decided to stop this project because one of the March 2025 updates mitigated stream interruption problem. The interruption doesn’t show from the beginning as it was before update but if you look at the streams for half an hour or so it still stops streaming.

Another (minor) reason why I stop this project is that there is no support from the community.