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;
|