Merge branch 'bugfix/streamContent_leaks' into 'v3'
ui_migration bugfix(MJPEG) fix logic and lifecycle leaks at StreamDisplay, MiniStreamDisplay keeping request open in the background. Closes #550 See merge request openflexure/openflexure-microscope-server!605
This commit is contained in:
commit
674e52e224
13 changed files with 204 additions and 55 deletions
|
|
@ -5,7 +5,7 @@
|
||||||
class="stream-display uk-width-1-1 uk-height-1-1 scrollTarget"
|
class="stream-display uk-width-1-1 uk-height-1-1 scrollTarget"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
v-if="isVisible"
|
v-show="isVisible && streamEnabled"
|
||||||
ref="click-frame"
|
ref="click-frame"
|
||||||
class="uk-align-center uk-margin-remove-bottom"
|
class="uk-align-center uk-margin-remove-bottom"
|
||||||
:src="streamImgUri"
|
:src="streamImgUri"
|
||||||
|
|
@ -19,33 +19,87 @@ import { useIntersectionObserver } from "@vueuse/core";
|
||||||
import { useSettingsStore } from "@/stores/settings.js";
|
import { useSettingsStore } from "@/stores/settings.js";
|
||||||
import { mapState } from "pinia";
|
import { mapState } from "pinia";
|
||||||
|
|
||||||
|
const ONE_PIXEL_FALLBACK = "data:image/PNG;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=";
|
||||||
|
|
||||||
// Export main app
|
// Export main app
|
||||||
export default {
|
export default {
|
||||||
name: "MiniStreamDisplay",
|
name: "MiniStreamDisplay",
|
||||||
|
|
||||||
|
props: {
|
||||||
|
streamId: {
|
||||||
|
type: [String],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
const store = useSettingsStore();
|
||||||
|
return {
|
||||||
|
store,
|
||||||
|
};
|
||||||
|
},
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
isVisible: false,
|
isVisible: false,
|
||||||
|
observerTimeout: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(useSettingsStore, ["baseUri"]),
|
...mapState(useSettingsStore, ["baseUri", "disableStream", "ready"]),
|
||||||
streamImgUri() {
|
streamEnabled: function () {
|
||||||
return `${this.baseUri}/camera/mjpeg_stream`;
|
return this.ready && !this.disableStream;
|
||||||
|
},
|
||||||
|
streamImgUri: function () {
|
||||||
|
// Require BOTH visibility AND the enabled state
|
||||||
|
if (this.isVisible && this.streamEnabled) {
|
||||||
|
const url = new URL(`${this.baseUri}/camera/mjpeg_stream`);
|
||||||
|
url.searchParams.append("streamId", this.streamId);
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force the browser to kill the connection by loading a 1 pixel image
|
||||||
|
return ONE_PIXEL_FALLBACK;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
useIntersectionObserver(
|
const { stop } = useIntersectionObserver(
|
||||||
this.$refs.streamDisplay,
|
this.$refs.streamDisplay,
|
||||||
([{ isIntersecting }]) => {
|
([{ isIntersecting }]) => {
|
||||||
|
// Always update local visibility instantly
|
||||||
this.isVisible = isIntersecting;
|
this.isVisible = isIntersecting;
|
||||||
|
|
||||||
|
// Clear any previous pending timers to prevent ghost fires
|
||||||
|
clearTimeout(this.observerTimeout);
|
||||||
|
|
||||||
|
if (isIntersecting) {
|
||||||
|
// If it's TRUE, we don't want to wait! Turn the stream on immediately.
|
||||||
|
this.store.addStream(this.streamId);
|
||||||
|
} else {
|
||||||
|
// If it's FALSE, wait 50ms before telling the store.
|
||||||
|
// If a TRUE event fires in the next 50ms (like during a tab switch),
|
||||||
|
// this timeout will be cancelled and the stream will never flicker off!
|
||||||
|
this.observerTimeout = setTimeout(() => {
|
||||||
|
this.store.removeStream(this.streamId);
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{ threshold: 0.0 },
|
||||||
threshold: 0.0,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
this.stopIntersectionObserver = stop;
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeUnmount() {
|
||||||
|
clearTimeout(this.observerTimeout); // Clean up the timer!
|
||||||
|
if (this.stopIntersectionObserver) {
|
||||||
|
this.stopIntersectionObserver();
|
||||||
|
}
|
||||||
|
this.store.removeStream(this.streamId);
|
||||||
|
// Override img source to single pixel and drop MJPEG stream
|
||||||
|
const imgElement = this.$refs["click-frame"];
|
||||||
|
if (imgElement) {
|
||||||
|
imgElement.src = ONE_PIXEL_FALLBACK;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,11 @@
|
||||||
<div ref="modal" class="" uk-modal="bg-close: false; esc-close: false; stack: true;">
|
<div ref="modal" class="" uk-modal="bg-close: false; esc-close: false; stack: true;">
|
||||||
<div id="status-modal" class="uk-modal-dialog uk-modal-body uk-margin-auto-vertical">
|
<div id="status-modal" class="uk-modal-dialog uk-modal-body uk-margin-auto-vertical">
|
||||||
<h2>{{ title }}</h2>
|
<h2>{{ title }}</h2>
|
||||||
<mini-stream-display v-if="displayStream" class="uk-margin-small-bottom" />
|
<mini-stream-display
|
||||||
|
v-if="displayStream"
|
||||||
|
:stream-id="setStreamId"
|
||||||
|
class="uk-margin-small-bottom"
|
||||||
|
/>
|
||||||
<action-log-display :log="log" :task-status="taskStatus" />
|
<action-log-display :log="log" :task-status="taskStatus" />
|
||||||
<div id="progress-and-cancel-row">
|
<div id="progress-and-cancel-row">
|
||||||
<div class="stretchy">
|
<div class="stretchy">
|
||||||
|
|
@ -79,6 +83,13 @@ export default {
|
||||||
|
|
||||||
emits: ["terminateTask"],
|
emits: ["terminateTask"],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
// This adds the parent name as value for prop streamId
|
||||||
|
setStreamId: this.$options.name,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
show() {
|
show() {
|
||||||
UIkit.modal(this.$refs.modal).show();
|
UIkit.modal(this.$refs.modal).show();
|
||||||
|
|
|
||||||
|
|
@ -68,8 +68,8 @@ export default {
|
||||||
this.$refs["calibrationModalEl"].addEventListener("hidden", this.onHide);
|
this.$refs["calibrationModalEl"].addEventListener("hidden", this.onHide);
|
||||||
// Check which Things are available on mount.
|
// Check which Things are available on mount.
|
||||||
const allCalibrationTasks = {
|
const allCalibrationTasks = {
|
||||||
camera: cameraCalibrationTask,
|
camera: markRaw(cameraCalibrationTask),
|
||||||
camera_stage_mapping: cameraStageMappingTask,
|
camera_stage_mapping: markRaw(cameraStageMappingTask),
|
||||||
};
|
};
|
||||||
this.availableCalibrationTasks = Object.fromEntries(
|
this.availableCalibrationTasks = Object.fromEntries(
|
||||||
Object.entries(allCalibrationTasks).filter(([thing]) => this.thingAvailable(thing)),
|
Object.entries(allCalibrationTasks).filter(([thing]) => this.thingAvailable(thing)),
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import runCsmStep from "./csmSteps/runCsmStep.vue";
|
||||||
import { markRaw } from "vue";
|
import { markRaw } from "vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "CameraCalibrationTask",
|
name: "CameraStageMappingTask",
|
||||||
components: {
|
components: {
|
||||||
calibrationWizardTask,
|
calibrationWizardTask,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
<miniStreamDisplay class="mini-preview" />
|
<miniStreamDisplay class="mini-preview" :stream-id="setStreamId" />
|
||||||
<slot name="below-stream"></slot>
|
<slot name="below-stream"></slot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -15,6 +15,13 @@ export default {
|
||||||
components: {
|
components: {
|
||||||
miniStreamDisplay,
|
miniStreamDisplay,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
// This adds the parent name as value for prop streamId
|
||||||
|
setStreamId: this.$options.name,
|
||||||
|
};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ mode.
|
||||||
</div>
|
</div>
|
||||||
<div v-if="taskStarted">
|
<div v-if="taskStarted">
|
||||||
<h2 v-if="taskInfoTitle" style="text-align: center">{{ taskInfoTitle }}</h2>
|
<h2 v-if="taskInfoTitle" style="text-align: center">{{ taskInfoTitle }}</h2>
|
||||||
<mini-stream-display v-if="taskInfoStream" />
|
<mini-stream-display v-if="taskInfoStream" :stream-id="setStreamId" />
|
||||||
<action-log-display id="log-display" :log="log" :task-status="taskStatus" />
|
<action-log-display id="log-display" :log="log" :task-status="taskStatus" />
|
||||||
<div class="uk-width-1-1 uk-flex uk-flex-center">
|
<div class="uk-width-1-1 uk-flex uk-flex-center">
|
||||||
<div class="uk-width-2-3">
|
<div class="uk-width-2-3">
|
||||||
|
|
@ -141,6 +141,7 @@ export default {
|
||||||
progress: null,
|
progress: null,
|
||||||
log: [],
|
log: [],
|
||||||
pollTimer: null,
|
pollTimer: null,
|
||||||
|
setStreamId: this.$options.name,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ import axios from "axios";
|
||||||
import ActionButton from "../../labThingsComponents/actionButton.vue";
|
import ActionButton from "../../labThingsComponents/actionButton.vue";
|
||||||
import positionControl from "./positionControl.vue";
|
import positionControl from "./positionControl.vue";
|
||||||
import autofocusControl from "./autofocusControl.vue";
|
import autofocusControl from "./autofocusControl.vue";
|
||||||
|
import { eventBus } from "../../../eventBus.js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "PaneControl",
|
name: "PaneControl",
|
||||||
|
|
@ -61,8 +62,12 @@ export default {
|
||||||
this.modalError("No image URI returned from capture task.");
|
this.modalError("No image URI returned from capture task.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Emit flash capture stream animation
|
||||||
|
eventBus.emit("globalFlashStream");
|
||||||
|
|
||||||
// To save the returned data, we make a virtual link and click it
|
// To save the returned data, we make a virtual link and click it
|
||||||
let imageResponse = await axios.get(imageUri, { responseType: "blob" });
|
let imageResponse = await axios.get(imageUri, { responseType: "blob" });
|
||||||
|
|
||||||
const url = window.URL.createObjectURL(new Blob([imageResponse.data]));
|
const url = window.URL.createObjectURL(new Blob([imageResponse.data]));
|
||||||
const link = document.createElement("a");
|
const link = document.createElement("a");
|
||||||
link.href = url;
|
link.href = url;
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
<paneControl />
|
<paneControl />
|
||||||
</div>
|
</div>
|
||||||
<div class="view-component uk-width-expand">
|
<div class="view-component uk-width-expand">
|
||||||
<streamDisplay />
|
<streamDisplay :stream-id="setStreamId" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -21,6 +21,13 @@ export default {
|
||||||
paneControl,
|
paneControl,
|
||||||
streamDisplay,
|
streamDisplay,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
// This adds the parent name as value for prop streamId
|
||||||
|
setStreamId: this.$options.name,
|
||||||
|
};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
<CSMCalibrationSettings />
|
<CSMCalibrationSettings />
|
||||||
</div>
|
</div>
|
||||||
<div id="mini-stream">
|
<div id="mini-stream">
|
||||||
<miniStreamDisplay />
|
<miniStreamDisplay :stream-id="setStreamId" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -28,6 +28,13 @@ export default {
|
||||||
CSMCalibrationSettings,
|
CSMCalibrationSettings,
|
||||||
miniStreamDisplay,
|
miniStreamDisplay,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
// This adds the parent name as value for prop streamId
|
||||||
|
setStreamId: this.$options.name,
|
||||||
|
};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="mini-stream">
|
<div id="mini-stream">
|
||||||
<miniStreamDisplay />
|
<miniStreamDisplay :stream-id="setStreamId" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -41,6 +41,8 @@ export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
manualCameraSettings: [],
|
manualCameraSettings: [],
|
||||||
|
// This adds the parent name as value for prop streamId
|
||||||
|
setStreamId: this.$options.name,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
class="image-fit"
|
class="image-fit"
|
||||||
:src="lastStitchedImage"
|
:src="lastStitchedImage"
|
||||||
/>
|
/>
|
||||||
<streamDisplay v-else />
|
<streamDisplay v-else :stream-id="setStreamId" />
|
||||||
<template #controls>
|
<template #controls>
|
||||||
<slideScanControls />
|
<slideScanControls />
|
||||||
<label class="uk-form-label" for="form-stacked-text">Sample ID</label>
|
<label class="uk-form-label" for="form-stacked-text">Sample ID</label>
|
||||||
|
|
@ -81,6 +81,8 @@ export default {
|
||||||
lastStitchedImage: null,
|
lastStitchedImage: null,
|
||||||
scanComplete: false,
|
scanComplete: false,
|
||||||
scan_name: "",
|
scan_name: "",
|
||||||
|
// This adds the parent name as value for prop streamId
|
||||||
|
setStreamId: this.$options.name,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,9 @@
|
||||||
class="stream-display uk-width-1-1 uk-height-1-1 scrollTarget"
|
class="stream-display uk-width-1-1 uk-height-1-1 scrollTarget"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
v-if="isVisible"
|
v-show="isVisible && streamEnabled"
|
||||||
ref="click-frame"
|
ref="click-frame"
|
||||||
class="uk-align-center uk-margin-remove-bottom"
|
class="uk-align-center uk-margin-remove-bottom"
|
||||||
:hidden="!streamEnabled"
|
|
||||||
:src="streamImgUri"
|
:src="streamImgUri"
|
||||||
alt="Stream"
|
alt="Stream"
|
||||||
@dblclick="clickMonitor"
|
@dblclick="clickMonitor"
|
||||||
|
|
@ -36,10 +35,19 @@ import { useIntersectionObserver } from "@vueuse/core";
|
||||||
import { useSettingsStore } from "@/stores/settings.js";
|
import { useSettingsStore } from "@/stores/settings.js";
|
||||||
import { mapState } from "pinia";
|
import { mapState } from "pinia";
|
||||||
|
|
||||||
|
const ONE_PIXEL_FALLBACK = "data:image/PNG;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=";
|
||||||
|
|
||||||
// Export main app
|
// Export main app
|
||||||
export default {
|
export default {
|
||||||
name: "StreamDisplay",
|
name: "StreamDisplay",
|
||||||
|
|
||||||
|
props: {
|
||||||
|
streamId: {
|
||||||
|
type: [String],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
const store = useSettingsStore();
|
const store = useSettingsStore();
|
||||||
return {
|
return {
|
||||||
|
|
@ -52,7 +60,7 @@ export default {
|
||||||
isVisible: false,
|
isVisible: false,
|
||||||
displaySize: [0, 0],
|
displaySize: [0, 0],
|
||||||
displayPosition: [0, 0],
|
displayPosition: [0, 0],
|
||||||
resizeTimeoutId: setTimeout(this.doneResizing, 500),
|
resizeTimeoutId: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -61,42 +69,47 @@ export default {
|
||||||
streamEnabled: function () {
|
streamEnabled: function () {
|
||||||
return this.ready && !this.disableStream;
|
return this.ready && !this.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 () {
|
streamImgUri: function () {
|
||||||
return `${this.baseUri}/camera/mjpeg_stream`;
|
// Only request the real stream if it's enabled AND currently visible on screen
|
||||||
|
if (this.isVisible && this.streamEnabled) {
|
||||||
|
const url = new URL(`${this.baseUri}/camera/mjpeg_stream`);
|
||||||
|
url.searchParams.append("streamId", this.streamId);
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force the browser to kill the connection by loading a 1 pixel image
|
||||||
|
return ONE_PIXEL_FALLBACK;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
|
// Initial Timeout
|
||||||
|
this.resizeTimeoutId = setTimeout(this.handleDoneResize, 500);
|
||||||
|
|
||||||
// set up an intersection observer
|
// set up an intersection observer
|
||||||
useIntersectionObserver(
|
// We capture the 'stop' function returned by VueUse so we can kill it later
|
||||||
|
const { stop } = useIntersectionObserver(
|
||||||
this.$refs.streamDisplay,
|
this.$refs.streamDisplay,
|
||||||
([{ isIntersecting }]) => {
|
([{ isIntersecting }]) => {
|
||||||
this.isVisible = isIntersecting;
|
this.isVisible = isIntersecting;
|
||||||
},
|
},
|
||||||
{
|
{ threshold: 0.0 },
|
||||||
threshold: 0.0,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
this.stopIntersectionObserver = stop;
|
||||||
// A global signal listener to flash the stream element
|
// A global signal listener to flash the stream element
|
||||||
this.onFlashStream = () => {
|
eventBus.on("globalFlashStream", this.flashStream);
|
||||||
this.flashStream();
|
|
||||||
};
|
|
||||||
|
|
||||||
eventBus.on("globalFlashStream", this.onFlashStream);
|
|
||||||
|
|
||||||
// Mutation observer
|
// Mutation observer
|
||||||
this.sizeObserver = new ResizeObserver(() => {
|
this.sizeObserver = new ResizeObserver(() => {
|
||||||
this.handleResize(); // For any element attached to the observer, run handleResize() on change
|
this.handleResize(); // For any element attached to the observer, run handleResize() on change
|
||||||
});
|
});
|
||||||
// Fetch streamDisplay component by ref
|
|
||||||
if (this.$refs.streamDisplay && this.$refs.streamDisplay.parentNode) {
|
// Safely fetch the DOM element (handles both native HTML and Vue components)
|
||||||
const streamDisplayElement = this.$refs.streamDisplay.parentNode;
|
const streamElement = this.$refs.streamDisplay?.$el || this.$refs.streamDisplay;
|
||||||
|
|
||||||
|
if (streamElement && streamElement.parentNode) {
|
||||||
// Attach streamDisplay to the size observer
|
// Attach streamDisplay to the size observer
|
||||||
this.sizeObserver.observe(streamDisplayElement);
|
this.sizeObserver.observe(streamElement.parentNode);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -104,24 +117,58 @@ export default {
|
||||||
// Do nothing: preview stream now runs all the time
|
// Do nothing: preview stream now runs all the time
|
||||||
},
|
},
|
||||||
|
|
||||||
beforeUnmount: function () {
|
beforeUnmount() {
|
||||||
// Disconnect the size observer
|
// Kill the resize timeout
|
||||||
|
clearTimeout(this.resizeTimeoutId);
|
||||||
|
|
||||||
|
// Kill the flash animation timeout (Fixes Beth's MR comment)
|
||||||
|
if (this.flashTimeoutId) {
|
||||||
|
clearTimeout(this.flashTimeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kill the Intersection Observer
|
||||||
|
if (this.stopIntersectionObserver) {
|
||||||
|
this.stopIntersectionObserver();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unregister the event bus listener
|
||||||
|
eventBus.off("globalFlashStream", this.flashStream);
|
||||||
|
|
||||||
|
// Disconnect the size Observer
|
||||||
|
if (this.sizeObserver) {
|
||||||
this.sizeObserver.disconnect();
|
this.sizeObserver.disconnect();
|
||||||
// Remove from the array of active streams
|
this.store.removeStream(this.streamId);
|
||||||
this.store.removeStream(this.$.uid);
|
}
|
||||||
// close signal
|
const imgElement = this.$refs["click-frame"];
|
||||||
eventBus.off("globalFlashStream", this.onFlashStream);
|
if (imgElement) {
|
||||||
|
imgElement.src = ONE_PIXEL_FALLBACK;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
flashStream: function () {
|
flashStream: function () {
|
||||||
// Run an animation that flashes the stream (for capture feedback)
|
// Run an animation that flashes the stream (for capture feedback)
|
||||||
let element = this.$refs.streamDisplay;
|
let element = this.$refs.streamDisplay;
|
||||||
|
|
||||||
|
// Defensive check in case the ref isn't available
|
||||||
|
if (!element) return;
|
||||||
|
|
||||||
element.classList.remove("uk-animation-fade");
|
element.classList.remove("uk-animation-fade");
|
||||||
element.offsetHeight; /* trigger reflow */
|
/* trigger reflow */
|
||||||
|
element.offsetHeight;
|
||||||
element.classList.add("uk-animation-fade");
|
element.classList.add("uk-animation-fade");
|
||||||
setTimeout(function () {
|
|
||||||
|
// Clear the timer if flashStream is called again before the 800ms is up
|
||||||
|
if (this.flashTimeoutId) {
|
||||||
|
clearTimeout(this.flashTimeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track the timeout ID
|
||||||
|
this.flashTimeoutId = setTimeout(() => {
|
||||||
|
// Extra defensive check just in case
|
||||||
|
if (element && element.classList) {
|
||||||
element.classList.remove("uk-animation-fade");
|
element.classList.remove("uk-animation-fade");
|
||||||
|
}
|
||||||
}, 800);
|
}, 800);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -158,17 +205,16 @@ export default {
|
||||||
|
|
||||||
handleDoneResize: function () {
|
handleDoneResize: function () {
|
||||||
// Recalculate size
|
// Recalculate size
|
||||||
|
|
||||||
this.recalculateSize();
|
this.recalculateSize();
|
||||||
// Handle closed stream
|
// Handle closed stream
|
||||||
if (this.displaySize[0] == 0 && this.displaySize[1] == 0) {
|
if (this.displaySize[0] == 0 && this.displaySize[1] == 0) {
|
||||||
// If this stream was previously active
|
// If this stream was previously active
|
||||||
if (this.store.activeStreams[this.$.uid] == true) {
|
if (this.store.activeStreams[this.streamId] == true) {
|
||||||
this.store.removeStream(this.$.uid);
|
this.store.removeStream(this.streamId);
|
||||||
}
|
}
|
||||||
// If resized to anything other than zero
|
// If resized to anything other than zero
|
||||||
} else {
|
} else {
|
||||||
this.store.addStream(this.$.uid);
|
this.store.addStream(this.streamId);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<!-- Grid managing tab content -->
|
<!-- Grid managing tab content -->
|
||||||
<div uk-grid class="uk-height-1-1 uk-margin-remove uk-padding-remove">
|
<div uk-grid class="uk-height-1-1 uk-margin-remove uk-padding-remove">
|
||||||
<div class="view-component uk-width-expand">
|
<div class="view-component uk-width-expand">
|
||||||
<streamDisplay />
|
<streamDisplay :stream-id="setStreamId" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -16,5 +16,12 @@ export default {
|
||||||
components: {
|
components: {
|
||||||
streamDisplay,
|
streamDisplay,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
// This adds the parent name as value for prop streamId
|
||||||
|
setStreamId: this.$options.name,
|
||||||
|
};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue