126 lines
3.1 KiB
Vue
126 lines
3.1 KiB
Vue
<template>
|
|
<div
|
|
id="stream-display"
|
|
ref="streamDisplay"
|
|
class="stream-display uk-width-1-1 uk-height-1-1 scrollTarget"
|
|
>
|
|
<img
|
|
v-show="isVisible && streamEnabled"
|
|
ref="click-frame"
|
|
class="uk-align-center uk-margin-remove-bottom"
|
|
:src="streamImgUri"
|
|
alt="Stream"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import { useIntersectionObserver } from "@vueuse/core";
|
|
import { useSettingsStore } from "@/stores/settings.js";
|
|
import { mapState } from "pinia";
|
|
|
|
const ONE_PIXEL_FALLBACK = "data:image/PNG;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=";
|
|
|
|
// Export main app
|
|
export default {
|
|
name: "MiniStreamDisplay",
|
|
|
|
props: {
|
|
streamId: {
|
|
type: [String],
|
|
required: true,
|
|
},
|
|
},
|
|
setup() {
|
|
const store = useSettingsStore();
|
|
return {
|
|
store,
|
|
};
|
|
},
|
|
data: function () {
|
|
return {
|
|
isVisible: false,
|
|
observerTimeout: null,
|
|
};
|
|
},
|
|
|
|
computed: {
|
|
...mapState(useSettingsStore, ["baseUri", "disableStream", "ready"]),
|
|
streamEnabled: function () {
|
|
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() {
|
|
const { stop } = useIntersectionObserver(
|
|
this.$refs.streamDisplay,
|
|
([{ isIntersecting }]) => {
|
|
// Always update local visibility instantly
|
|
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 },
|
|
);
|
|
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>
|
|
|
|
<style scoped lang="less">
|
|
.stream-display img {
|
|
text-align: center;
|
|
object-fit: contain;
|
|
border: 1px solid #555;
|
|
}
|
|
|
|
.stream-display {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
.position-relative {
|
|
position: relative !important;
|
|
}
|
|
|
|
.text-center {
|
|
text-align: center;
|
|
}
|
|
</style>
|