diff --git a/.styleguide b/.styleguide index 72a1cf0e99..cf50b88396 100644 --- a/.styleguide +++ b/.styleguide @@ -13,6 +13,7 @@ modifiableFileExclude { \.jpg$ \.jpeg$ \.png$ + \.gif$ \.so$ \.dll$ } diff --git a/photon-client/src/assets/loading.gif b/photon-client/src/assets/loading.gif new file mode 100644 index 0000000000..2dc1e701a7 Binary files /dev/null and b/photon-client/src/assets/loading.gif differ diff --git a/photon-client/src/assets/noStream.jpg b/photon-client/src/assets/noStream.jpg index b3b4f09b2a..b3a28512d6 100644 Binary files a/photon-client/src/assets/noStream.jpg and b/photon-client/src/assets/noStream.jpg differ diff --git a/photon-client/src/components/common/cv-image.vue b/photon-client/src/components/common/cv-image.vue index 366cc0827c..240e5a89c2 100644 --- a/photon-client/src/components/common/cv-image.vue +++ b/photon-client/src/components/common/cv-image.vue @@ -5,7 +5,7 @@ :style="styleObject" :src="src" alt="" - @click="e => $emit('click', e)" + @click="e => {this.openThinclientStream(e)}" > @@ -13,7 +13,7 @@ export default { name: "CvImage", // eslint-disable-next-line vue/require-prop-types - props: ['address', 'scale', 'maxHeight', 'maxHeightMd', 'maxHeightLg', 'maxHeightXl', 'colorPicking', 'id', 'disconnected'], + props: ['idx', 'scale', 'maxHeight', 'maxHeightMd', 'maxHeightLg', 'maxHeightXl', 'colorPicking', 'id', 'disconnected'], data() { return { seed: 1.0, @@ -46,18 +46,48 @@ return ret; } }, - src: { + port: { get() { - return this.disconnected ? require("../../assets/noStream.jpg") : this.address + "?" + this.seed // This prevents caching - }, - }, + if(this.idx == 0){ + return this.$store.state.cameraSettings[this.$store.state.currentCameraIndex].inputStreamPort; + } else { + return this.$store.state.cameraSettings[this.$store.state.currentCameraIndex].outputStreamPort; + } + } + } + }, + watch : { + port(newPort, oldPort){ + newPort; + oldPort; + this.reload(); + }, + disconnected(newVal, oldVal){ + oldVal; + if(newVal){ + this.wsStream.stopStream(); + } else { + this.wsStream.startStream(); + } + } }, mounted() { - this.reload(); // Force reload image on creation + var wsvs = require('../../plugins/WebsocketVideoStream'); + this.wsStream = new wsvs.WebsocketVideoStream(this.id, this.port, window.location.host); + }, + unmounted() { + this.wsStream.stopStream(); + this.wsStream.ws_close(); }, methods: { reload() { - this.seed = new Date().getTime(); + console.log("Reloading " + this.id + " with port " + String(this.port)); + this.wsStream.setPort(this.port); + }, + openThinclientStream(e){ + e; + var URL = "/thinclient.html?port=" + String(this.port) + "&host=" + window.location.hostname; + window.open(URL, '_blank'); } }, } diff --git a/photon-client/src/main.js b/photon-client/src/main.js index 81248f7c72..fd1bcba833 100644 --- a/photon-client/src/main.js +++ b/photon-client/src/main.js @@ -15,11 +15,11 @@ if (process.env.NODE_ENV === "production") { Vue.prototype.$address = location.hostname + ":5800"; } -const wsURL = '//' + Vue.prototype.$address + '/websocket'; +const wsDataURL = '//' + Vue.prototype.$address + '/websocket_data'; import VueNativeSock from 'vue-native-websocket'; -Vue.use(VueNativeSock, wsURL, { +Vue.use(VueNativeSock, wsDataURL, { reconnection: true, reconnectionDelay: 100, connectManually: true, diff --git a/photon-client/src/plugins/ColorPicker.js b/photon-client/src/plugins/ColorPicker.js index b887fd2da0..8c58235d24 100644 --- a/photon-client/src/plugins/ColorPicker.js +++ b/photon-client/src/plugins/ColorPicker.js @@ -5,7 +5,7 @@ function initColorPicker() { if (!canvas) canvas = document.createElement('canvas'); - image = document.querySelector('#normal-stream'); + image = document.querySelector('#raw-stream'); if (image !== null) { canvas.width = image.width; canvas.height = image.height; diff --git a/photon-client/src/plugins/WebsocketVideoStream.js b/photon-client/src/plugins/WebsocketVideoStream.js new file mode 100644 index 0000000000..7a409e8dde --- /dev/null +++ b/photon-client/src/plugins/WebsocketVideoStream.js @@ -0,0 +1,148 @@ + + +export class WebsocketVideoStream{ + + + constructor(drawDiv, streamPort, host) { + + this.drawDiv = drawDiv; + this.image = document.getElementById(this.drawDiv); + this.streamPort = streamPort; + this.serverAddr = "ws://" + host + "/websocket_cameras"; + this.noStream = false; + this.noStreamPrev = false; + this.setNoStream(); + this.ws_connect(); + this.imgData = null; + this.imgDataTime = -1; + this.imgObjURL = null; + this.frameRxCount = 0; + + requestAnimationFrame(()=>this.animationLoop()); + + } + + animationLoop(){ + var now = window.performance.now(); + + if((now - this.imgDataTime) > 2500 && this.imgData != null){ + //Handle websocket send timeouts by restarting + this.setNoStream(); + this.stopStream(); + setTimeout(this.startStream.bind(this), 1000); //restart stream one second later + } else { + if(this.streamPort == null){ + this.setNoStream(); + } else if (this.imgData != null) { + //From https://stackoverflow.com/questions/67507616/set-image-src-from-image-blob/67507685#67507685 + if(this.imgObjURL != null){ + URL.revokeObjectURL(this.imgObjURL) + } + this.imgObjURL = URL.createObjectURL(this.imgData); + + //Update the image with the new mimetype and image + this.image.src = this.imgObjURL; + this.noStream = false; + + } else { + //Nothing, hold previous image while waiting for next frame + } + } + + + requestAnimationFrame(()=>this.animationLoop()); + } + + setNoStream() { + this.noStreamPrev = this.noStream; + this.noStream = true; + if(this.noStreamPrev == false && this.noStream == true){ + //One-shot background change to preserve animation + this.image.src = require("../assets/loading.gif"); + } + } + + startStream() { + if(this.serverConnectionActive == true && this.streamPort > 0){ + this.ws.send(JSON.stringify({"cmd": "subscribe", "port":this.streamPort})); + this.noStream = false; + } + } + + stopStream() { + if(this.serverConnectionActive == true && this.streamPort > 0){ + this.ws.send(JSON.stringify({"cmd": "unsubscribe"})); + this.noStream = true; + } + } + + setPort(streamPort){ + this.stopStream(); + this.frameRxCount = 0; + this.streamPort = streamPort; + this.startStream(); + } + + ws_onOpen() { + // Set the flag allowing general server communication + this.serverConnectionActive = true; + console.log("Connected!"); + this.startStream(); + } + + ws_onClose(e) { + this.setNoStream(); + + //Clear flags to stop server communication + this.ws = null; + this.serverConnectionActive = false; + + console.log('Camera Socket is closed. Reconnect will be attempted in 0.5 second.', e.reason); + setTimeout(this.ws_connect.bind(this), 500); + + if(!e.wasClean){ + console.error('Socket encountered error!'); + } + + } + + ws_onError(e){ + e; //prevent unused failure + this.ws.close(); + } + + ws_onMessage(e){ + if(typeof e.data === 'string'){ + //string data from host + //TODO - anything to receive info here? Maybe "available streams?" + } else { + if(e.data.size > 0){ + //binary data - a frame + this.imgData = e.data; + this.imgDataTime = window.performance.now(); + this.frameRxCount++; + } else { + //TODO - server is sending empty frames? + } + } + + } + + ws_connect() { + this.ws = new WebSocket(this.serverAddr); + this.ws.binaryType = "blob"; + this.ws.onopen = this.ws_onOpen.bind(this); + this.ws.onmessage = this.ws_onMessage.bind(this); + this.ws.onclose = this.ws_onClose.bind(this); + this.ws.onerror = this.ws_onError.bind(this); + console.log("Connecting to server " + this.serverAddr); + } + + ws_close(){ + this.ws.close(); + } + +} + + +export default {WebsocketVideoStream} diff --git a/photon-client/src/store/index.js b/photon-client/src/store/index.js index 086b75260e..fc0cc2e540 100644 --- a/photon-client/src/store/index.js +++ b/photon-client/src/store/index.js @@ -35,8 +35,8 @@ export default new Vuex.Store({ tiltDegrees: 0.0, currentPipelineIndex: 0, pipelineNicknames: ["Unknown"], - outputStreamPort: 1181, - inputStreamPort: 1182, + outputStreamPort: 0, + inputStreamPort: 0, nickname: "Unknown", videoFormatList: [ { diff --git a/photon-client/src/views/CamerasView.vue b/photon-client/src/views/CamerasView.vue index 39a8f9dfdf..3a5f2a5789 100644 --- a/photon-client/src/views/CamerasView.vue +++ b/photon-client/src/views/CamerasView.vue @@ -291,7 +291,8 @@ >