341 lines
11 KiB
Vue
341 lines
11 KiB
Vue
<template>
|
|
<div
|
|
id="stream-display"
|
|
ref="streamDisplay"
|
|
v-observe-visibility="visibilityChanged"
|
|
class="stream-display uk-width-1-1 uk-height-1-1 scrollTarget"
|
|
>
|
|
<img
|
|
v-if="isVisible"
|
|
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 {
|
|
isVisible: false,
|
|
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);
|
|
});
|
|
this.$root.$on("globalSafeTogglePreview", state => {
|
|
this.safePreviewRequest(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.safePreviewRequest(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: {
|
|
visibilityChanged(isVisible) {
|
|
this.isVisible = isVisible;
|
|
},
|
|
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");
|
|
setTimeout(function() {
|
|
element.classList.remove("uk-animation-fade");
|
|
}, 800);
|
|
},
|
|
|
|
clickMonitor: function(event) {
|
|
// Calculate steps from event coordinates and store config FOV
|
|
let xCoordinate = event.offsetX;
|
|
let yCoordinate = event.offsetY;
|
|
|
|
// Simply scaling by naturalHeight/offsetHeight may give the wrong answer!
|
|
// because we use content-fit: contain in the stylesheet, the img element
|
|
// may be larger than the picture. So, we must determine whether the
|
|
// width or height is setting the scaling factor, and use a uniform scale
|
|
// factor.
|
|
let scale = Math.max(
|
|
event.target.naturalWidth / event.target.offsetWidth,
|
|
event.target.naturalHeight / event.target.offsetHeight
|
|
);
|
|
|
|
let xRelative = (0.5 * event.target.offsetWidth - xCoordinate) * scale;
|
|
let yRelative = (0.5 * event.target.offsetHeight - yCoordinate) * scale;
|
|
|
|
// 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.safePreviewRequest(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) {
|
|
// Start the preview immediately
|
|
this.safePreviewRequest(true);
|
|
// Send another start preview request after a timeout
|
|
/*
|
|
The internal logic of this component means that a stop GPU preview
|
|
request will only ever be sent if ALL stream components are invisible.
|
|
However, when switching tabs, sometimes the previous component will
|
|
react to becoming invisible before the new tab has become visible.
|
|
This results in a stop-preview request being sent JUST before the new
|
|
start preview request is sent. In an ideal network, this is fine, however
|
|
due to network jitter, these requests will occasionally be recieved out
|
|
of order, causing the preview to start in the new location, and then
|
|
quickly stop again.
|
|
Preventing this properly will involve some clever server-side logic
|
|
probably, however as a pit-stop solution, we always send another
|
|
start-preview request after 1 second. This means that either:
|
|
1. The initial requests happened in order, in which case this follow-up
|
|
request does nothing, or
|
|
2. The requests leapfrog eachother, stopping the preview, in which case
|
|
the follow-up request restarts the preview in the correct location.
|
|
*/
|
|
setTimeout(() => this.safePreviewRequest(true), 250);
|
|
}
|
|
}
|
|
},
|
|
|
|
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;
|
|
},
|
|
|
|
safePreviewRequest: function(state) {
|
|
// previewRequest, but only stopping preview if all streams are invisible
|
|
// and only starting preview if any stream is visible
|
|
var a = Object.values(this.$store.state.activeStreams);
|
|
let allClosed = a.every(v => v === false);
|
|
// If all streams are closed, don't start GPU preview
|
|
if (state === true && allClosed) {
|
|
return false;
|
|
}
|
|
// If not all streams are closed, don't stop GPU preview
|
|
if (state === false && !allClosed) {
|
|
return false;
|
|
}
|
|
return this.previewRequest(state);
|
|
},
|
|
|
|
previewRequest: function(state) {
|
|
// If requesting starting the stream, but this component is inactive, skip
|
|
if (
|
|
this.$store.getters.ready == true &&
|
|
this.$store.state.globalSettings.autoGpuPreview == 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>
|