openflexure-microscope-server/src/components/viewComponents/streamDisplay.vue
2020-05-14 15:31:19 +01:00

293 lines
8.7 KiB
Vue

<template>
<div
id="stream-display"
ref="streamDisplay"
class="stream-display uk-width-1-1 uk-height-1-1 scrollTarget"
>
<img
v-if="thisStreamOpen"
ref="click-frame"
class="uk-align-center uk-margin-remove-bottom"
:hidden="!streamEnabled"
:src="streamImgUri"
alt="Stream"
@dblclick="clickMonitor"
/>
<div v-if="!streamEnabled" class="uk-height-1-1">
<div v-if="$store.state.waiting" class="uk-position-center">
<div uk-spinner="ratio: 4.5"></div>
</div>
<div
v-else-if="$store.state.globalSettings.disableStream"
class="uk-position-center position-relative text-center"
>
Stream preview disabled
</div>
<div v-else class="uk-position-center position-relative text-center">
No active connection
</div>
</div>
</div>
</template>
<script>
import axios from "axios";
// Export main app
export default {
name: "StreamDisplay",
data: function() {
return {
displaySize: [0, 0],
displayPosition: [0, 0],
fov: [0, 0],
resizeTimeoutId: setTimeout(this.doneResizing, 500)
};
},
computed: {
streamEnabled: function() {
return (
this.$store.getters.ready &&
!this.$store.state.globalSettings.disableStream
);
},
thisStreamOpen: function() {
// Only a single MJPEG connection should be open at a time
return !(this.displaySize[0] == 0) && !(this.displaySize[1] == 0);
},
streamImgUri: function() {
return `${this.$store.getters.baseUri}/api/v2/streams/mjpeg`;
},
startPreviewUri: function() {
return `${this.$store.getters.baseUri}/api/v2/actions/camera/preview/start`;
},
stopPreviewUri: function() {
return `${this.$store.getters.baseUri}/api/v2/actions/camera/preview/stop`;
},
settingsUri: function() {
return `${this.$store.getters.baseUri}/api/v2/instrument/settings`;
}
},
mounted() {
console.log(`${this._uid} mounted`);
// A global signal listener to change the GPU preview state
this.$root.$on("globalTogglePreview", state => {
this.previewRequest(state);
});
// A global signal listener to flash the stream element
this.$root.$on("globalFlashStream", () => {
this.flashStream();
});
// Mutation observer
this.sizeObserver = new ResizeObserver(() => {
this.handleResize(); // For any element attached to the observer, run handleResize() on change
});
// Fetch streamDisplay component by ref
const streamDisplayElement = this.$refs.streamDisplay.parentNode;
// Attach streamDisplay to the size observer
this.sizeObserver.observe(streamDisplayElement);
},
created: function() {
console.log(`${this._uid} created`);
// Send a request to start/stop GPU preview based on global setting
this.previewRequest(this.$store.state.globalSettings.autoGpuPreview);
// Get FOV from settings
this.updateFov();
},
beforeDestroy: function() {
console.log(`${this._uid} being destroyed`);
// Remove global signal listener to change the GPU preview state
this.$root.$off("globalTogglePreview");
// Remove global signal listener to flash the stream element
this.$root.$off("globalFlashStream");
// Disconnect the size observer
this.sizeObserver.disconnect();
// Remove from the array of active streams
this.$store.commit("removeStream", this._uid);
},
methods: {
flashStream: function() {
// Run an animation that flashes the stream (for capture feedback)
let element = this.$refs.streamDisplay;
element.classList.remove("uk-animation-fade");
element.offsetHeight; /* trigger reflow */
element.classList.add("uk-animation-fade");
},
clickMonitor: function(event) {
// Calculate steps from event coordinates and store config FOV
let xCoordinate = event.offsetX;
let yCoordinate = event.offsetY;
let xRelative =
((0.5 * event.target.offsetWidth - xCoordinate) /
event.target.offsetWidth) *
event.target.naturalWidth;
let yRelative =
((0.5 * event.target.offsetHeight - yCoordinate) /
event.target.offsetHeight) *
event.target.naturalHeight;
// Emit a signal to move, acted on by panelNavigate.vue
this.$root.$emit(
"globalMoveInImageCoordinatesEvent",
-xRelative,
-yRelative
);
},
handleResize: function() {
// Only fires resize event after no resize in 500ms (prevents resize event spam)
clearTimeout(this.resizeTimeoutId);
this.resizeTimeoutId = setTimeout(this.handleDoneResize, 250);
},
handleDoneResize: function() {
// Recalculate size
console.log(`Recalculating frame size for ${this._uid}`);
this.recalculateSize();
// Handle closed stream
if (this.displaySize[0] == 0 && this.displaySize[1] == 0) {
console.log(`${this._uid} is zero`);
// If this stream was previously active
if (this.$store.state.activeStreams[this._uid] == true) {
console.log(`${this._uid} was active`);
console.log(`STREAM ${this._uid} CLOSED`);
this.$store.commit("removeStream", this._uid);
// If all streams are closed, request the GPU preview close
var a = Object.values(this.$store.state.activeStreams);
let allClosed = a.every(v => v === false);
if (allClosed) {
this.previewRequest(false);
}
}
// If resized to anything other than zero
} else {
console.log(`STREAM ${this._uid} OPEN`);
this.$store.commit("addStream", this._uid);
if (this.$store.state.globalSettings.autoGpuPreview == true) {
// Reload preview
this.previewRequest(true);
}
}
},
recalculateSize: function() {
// Calculate stream size
let element = this.$refs.streamDisplay.parentNode;
let bound = element.getBoundingClientRect();
let elementSize = [bound.width, bound.height];
let elementPositionOnWindow = [bound.left, bound.top];
let windowPositionOnDisplay = [window.screenX, window.screenY];
let windowChromeHeight = window.outerHeight - window.innerHeight;
let elementPositionOnDisplay = [
Math.max(0, windowPositionOnDisplay[0] + elementPositionOnWindow[0]),
Math.max(
0,
windowPositionOnDisplay[1] +
elementPositionOnWindow[1] +
windowChromeHeight
)
];
this.displaySize = elementSize;
this.displayPosition = elementPositionOnDisplay;
},
previewRequest: function(state) {
// If all streams are closed, don't start GPU preview
var a = Object.values(this.$store.state.activeStreams);
let allClosed = a.every(v => v === false);
if (state === true && allClosed) {
return false;
}
// If requesting starting the stream, but this component is inactive, skip
if (this.$store.getters.ready == true) {
var requestUri = null;
// Create URI
if (state == true) {
requestUri = this.startPreviewUri;
} else {
requestUri = this.stopPreviewUri;
}
// Generate payload if tracking window position
var payload = {};
if (
this.$store.state.globalSettings.trackWindow == true &&
state == true
) {
// Recalculate frame dimensions and position
this.recalculateSize();
// Copy data into payload array
payload = {
window: [
this.displayPosition[0],
this.displayPosition[1],
this.displaySize[0],
this.displaySize[1]
]
};
}
// Send preview request
console.log(`${this._uid} toggled preview to ${state}`);
axios
.post(requestUri, payload)
.then(() => {})
.catch(error => {
this.modalError(error); // Let mixin handle error
});
}
},
updateFov: function() {
console.log("Updating FOV");
// Get the current field-of-view setting from the server
axios
.get(`${this.settingsUri}/fov`)
.then(response => {
if (response.data) {
this.fov = response.data;
}
})
.catch(error => {
this.modalError(error); // Let mixin handle error
});
}
}
};
</script>
<style scoped lang="less">
.stream-display img {
height: 100%;
text-align: center;
object-fit: contain;
}
.stream-display {
width: 100%;
height: 100%;
}
.position-relative {
position: relative !important;
}
.text-center {
text-align: center;
}
</style>