LCOV - code coverage report
Current view: top level - www/src/components - CameraCard.vue.js (source / functions) Hit Total Coverage
Test: lcov.info Lines: 64 101 63.4 %
Date: 2024-07-30 12:54:47 Functions: 10 19 52.6 %

          Line data    Source code
       1             : // @ts-check
       2             : 
       3             : import Vue from 'vue';
       4             : import { isNil } from 'lodash';
       5             : 
       6             : import { BaseLogger as logger } from '@cern/base-vue';
       7             : import { httpToWs } from '../utilities';
       8             : import d from 'debug';
       9             : 
      10           1 : const debug = d('cam');
      11           1 : const WS_NORMAL_CLOSURE = 1000;
      12           1 : const RECONNECT_DELAY = 5000;
      13             : 
      14             : /**
      15             :  * @typedef {V.Instance<typeof component>} Instance
      16             :  */
      17             : 
      18           1 : const component = Vue.extend({
      19             :   name: 'CameraCard',
      20             :   props: {
      21             :     title: { type: String, default: 'CameraCard' },
      22             :     src: { type: String, default: '', required: true },
      23             :     description: { type: String, default: '' },
      24             :     useWS: { type: Boolean, default: false },
      25             :     showFps: { type: Boolean, default: false }
      26             :   },
      27             :   /**
      28             :    * @return {{ connected: boolean, nextFrame: boolean, ws: WebSocket|null, imgURL: string,
      29             :    *  fps: number, frames: number, timer: NodeJS.Timeout|null, reconnect: NodeJS.Timeout|null }}
      30             :    */
      31             :   data() {
      32          26 :     return {
      33             :       connected: false,
      34             :       nextFrame: false,
      35             :       ws: null,
      36             :       imgURL: '',
      37             :       frames: 0,
      38             :       fps: 0,
      39             :       timer: null,
      40             :       reconnect: null
      41             :     };
      42             :   },
      43             :   watch: {
      44             :     /** @this {Instance} */
      45           0 :     src() { this.reload(); },
      46             :     /**
      47             :      * @this {Instance}
      48             :      * @param {boolean} value
      49             :      */
      50           8 :     connected(value) { this.$emit("connected", value); }
      51             :   },
      52             :   /** @this {Instance} */
      53             :   mounted() {
      54          26 :     window.document
      55             :     .addEventListener('visibilitychange', this.onVisibilityChange);
      56          26 :     this.enable(true);
      57             :   },
      58             :   /** @this {Instance} */
      59             :   beforeDestroy() {
      60          26 :     window.document
      61             :     .removeEventListener('visibilitychange', this.onVisibilityChange);
      62          26 :     this.enable(false);
      63          26 :     if (this.timer) {
      64           0 :       clearInterval(this.timer);
      65             :     }
      66          26 :     if (this.reconnect) {
      67           0 :       clearTimeout(this.reconnect);
      68             :     }
      69             :   },
      70             :   methods: {
      71             :     /** @this {Instance} */
      72             :     onLoaded() {
      73           8 :       this.connected = true;
      74           8 :       this.nextFrame = true;
      75           8 :       this.frames++;
      76             :     },
      77             :     /** @this {Instance} */
      78             :     onError() {
      79          16 :       this.connected = false;
      80          16 :       this.nextFrame = true;
      81          16 :       if (this.imgURL) {
      82           0 :         logger.error(`Cannot display the stream from ${this.src}`);
      83             :       }
      84             :     },
      85             :     isVisible() {
      86           0 :       return document.visibilityState !== 'hidden';
      87             :     },
      88             :     /**
      89             :      * @this {Instance}
      90             :      * @param {boolean} visible
      91             :      */
      92             :     enable(visible) {
      93          52 :       if (visible) {
      94          26 :         this.fpsCount(true);
      95             : 
      96          26 :         if (this.useWS) {
      97          26 :           this.openWS(httpToWs(this.src));
      98             :         }
      99             :         else {
     100           0 :           this.imgURL = this.src;
     101             :         }
     102             :       }
     103             :       else {
     104          26 :         this.fpsCount(false);
     105             : 
     106             :         // stops the MJPEG stream over WebSocket (if any)
     107          26 :         if (this.ws) {
     108          26 :           const ws = this.ws;
     109          26 :           this.ws = null;
     110          26 :           ws.onerror = null;
     111          26 :           ws.onclose = null;
     112          26 :           ws.onmessage = null;
     113             : 
     114             :           // this.imgURL = '';
     115          26 :           this.connected = false;
     116          26 :           try {
     117          26 :             ws.close(WS_NORMAL_CLOSURE);
     118             :           }
     119             :           catch (err) {
     120           0 :             debug('websocket closure error:', err);
     121             :           }
     122             : 
     123          26 :           const imgURL = this.imgURL;
     124          26 :           this.imgURL = '';
     125          26 :           if (imgURL) {
     126           8 :             URL.revokeObjectURL(imgURL);
     127             :           }
     128             :         }
     129             :         else {
     130             :           // stops the MJPEG stream over HTTP (if any)
     131             :           // (see https://bugs.chromium.org/p/chromium/issues/detail?id=73395)
     132           0 :           this.imgURL = '';
     133             :         }
     134             :       }
     135             :     },
     136             :     /** @this {Instance} */
     137             :     onVisibilityChange() {
     138           0 :       switch (window.document.visibilityState) {
     139             :       case "hidden":
     140           0 :         this.enable(false);
     141           0 :         break;
     142             :       case "visible":
     143             :       default:
     144           0 :         this.enable(true);
     145           0 :         break;
     146             :       }
     147             :     },
     148             :     /**
     149             :      * @this {Instance}
     150             :      * @param {string} url
     151             :      */
     152             :     openWS(url) {
     153          26 :       if (this.ws) { return; }
     154          26 :       debug('openging camera socket %s', url);
     155          26 :       if (this.reconnect) {
     156           0 :         clearTimeout(this.reconnect);
     157           0 :         this.reconnect = null;
     158             :       }
     159             : 
     160          26 :       this.ws = new WebSocket(url);
     161          26 :       this.ws.onerror = () => logger.error(`WebSocket error [${this.title}]`);
     162          26 :       this.ws.onclose = (/** @type {CloseEvent} */e) => {
     163           0 :         if (e.code === WS_NORMAL_CLOSURE) { return; }
     164             : 
     165           0 :         logger.error(`WebSocket closed [${this.title}]: ${e.reason} (code: ${e.code})`);
     166           0 :         this.reconnect = setTimeout(() => {
     167           0 :           this.reconnect = null;
     168           0 :           this.enable(this.isVisible());
     169             :         }, RECONNECT_DELAY);
     170           0 :         URL.revokeObjectURL(this.imgURL);
     171           0 :         this.imgURL = '';
     172           0 :         this.connected = false;
     173           0 :         this.ws = null;
     174             :       };
     175          26 :       this.ws.onmessage = (/** @type {MessageEvent} */event) => {
     176          10 :         if (isNil(event.data)) {
     177           0 :           console.warn(`No data received [${this.title}]`);
     178           0 :           return;
     179             :         }
     180             : 
     181          10 :         if (typeof event.data === 'object') { // blob
     182           8 :           if (!this.nextFrame) { // frame processing not yet completed
     183           0 :             console.warn(`Frame dropped [${this.title}]`);
     184           0 :             return;
     185             :           }
     186             : 
     187             :           // load new frame
     188           8 :           this.nextFrame = false;
     189           8 :           const tmp = this.imgURL;
     190           8 :           this.imgURL = URL.createObjectURL(event.data);
     191           8 :           URL.revokeObjectURL(tmp);
     192             :         }
     193           2 :         else if (typeof event.data === 'string' &&
     194             :                  event.data.startsWith('{')) {
     195           2 :           const tmp = this.imgURL;
     196           2 :           this.imgURL = "";
     197           2 :           URL.revokeObjectURL(tmp);
     198           2 :           try {
     199             :             /** @type {string} */
     200           2 :             const errMsg = (JSON.parse(event.data)).data;
     201           2 :             logger.error(`WebSocket error [${this.title}]: ${errMsg}`);
     202             :           }
     203             :           catch (err) {
     204           0 :             logger.error(`WebSocket error [${this.title}]: ` +
     205             :             /** @type {Error}*/(err).message);
     206             :           }
     207             :         }
     208             :         else {
     209           0 :           logger.error(`WebSocket error [${this.title}]: invalid data`);
     210             :         }
     211             :       };
     212             :     },
     213             :     /**
     214             :      * @this {Instance}
     215             :      * @param {boolean} enable
     216             :      */
     217             :     fpsCount(enable) {
     218          52 :       if (!this.showFps) { return; }
     219             : 
     220          52 :       if (enable && isNil(this.timer)) {
     221          26 :         this.timer = setInterval(() => {
     222           0 :           this.fps = this.frames;
     223           0 :           this.frames = 0;
     224             :         }, 1000);
     225             :       }
     226          26 :       else if (!enable && this.timer) {
     227          26 :         clearInterval(this.timer);
     228          26 :         this.timer = null;
     229          26 :         this.frames = 0;
     230             :       }
     231             :     },
     232             :     /** @this {Instance} */
     233             :     reload() {
     234           0 :       debug('reloading camera: %s', this.src);
     235           0 :       this.enable(false);
     236           0 :       if (this.isVisible()) {
     237           0 :         this.enable(true);
     238             :       }
     239             :     },
     240             :     /**
     241             :      * @this {Instance}
     242             :      * @param {MouseEvent} event
     243             :      */
     244             :     onClick(event) {
     245           0 :       this.$emit("click", event);
     246             :     }
     247             :   }
     248             : });
     249             : 
     250             : export default component;

Generated by: LCOV version 1.16