Files
scrypted/plugins/core/ui/src/interfaces/CameraViewer.vue
2023-01-05 12:31:35 -08:00

256 lines
8.0 KiB
Vue

<template>
<div style="background: black; position: relative; overflow: hidden; width: 100%; height: 100%; display: flex;"
@wheel="doTimeScroll">
<ClipPathEditor v-if="clipPath" style="
background: transparent;
top: 0;
left: 0;
position: absolute;
width: 100%;
height: 100%;
z-index: 2;
" v-model="clipPath"></ClipPathEditor>
<div style="position: relative; width: 100%; height: 100%;" :class="clipPath ? 'clip-path' : undefined">
<video ref="video" style="
background-color: black;
width: 100%;
height: 100%;
z-index: 0;
-webkit-transform-style: preserve-3d;
" playsinline autoplay></video>
<svg :viewBox="`0 0 ${svgWidth} ${svgHeight}`" ref="svg" style="
top: 0;
left: 0;
position: absolute;
width: 100%;
height: 100%;
z-index: 1;
" v-html="svgContents"></svg>
<v-btn v-if="$isMobile()" @click="$emit('exitFullscreen')" small icon color="white"
style="position: absolute; top: 10px; right: 10px; z-index: 3">
<v-icon small>fa fa-times</v-icon></v-btn>
<div style="position: absolute; bottom: 10px; right: 10px; z-index: 3">
<v-dialog width="unset" v-model="dateDialog" v-if="showNvr">
<template v-slot:activator="{ on }">
<v-btn :dark="!isLive" v-on="on" small :color="isLive ? 'white' : 'blue'" :outlined="isLive">
<v-icon small color="white" :outlined="isLive">fa fa-calendar-alt</v-icon>&nbsp;{{ monthDay }}</v-btn>
</template>
<v-date-picker @input="datePicked"></v-date-picker>
</v-dialog>
<v-btn v-if="showNvr" :dark="!isLive" small :color="isLive ? 'white' : adjustingTime ? 'green' : 'blue'"
:outlined="isLive" @click="streamRecorder(Date.now() - 2 * 60 * 1000)">
<v-btn v-if="!isLive && adjustingTime" small :color="isLive ? 'white' : adjustingTime ? 'green' : 'blue'"
:outlined="isLive">
{{ time }}</v-btn>
<v-icon v-else small color="white" :outlined="isLive">fa fa-video</v-icon></v-btn>
<v-btn small v-if="isLive && hasIntercom" @click="toggleMute" color="white" outlined>
<v-icon v-if="muted" small color="white" :outlined="isLive">fa fa-microphone-slash
</v-icon>
<v-icon v-else small color="white" :outlined="isLive">fa fa-microphone
</v-icon>
</v-btn>
<v-btn v-if="showNvr" :dark="!isLive" small color="red" :outlined="!isLive" @click="streamCamera">Live</v-btn>
</div>
</div>
</div>
</template>
<script>
import { streamCamera, streamRecorder } from "../common/camera";
import { ScryptedInterface } from "@scrypted/types";
import ClipPathEditor from "../components/clippath/ClipPathEditor.vue";
import cloneDeep from "lodash/cloneDeep";
import { datePickerLocalTimeToUTC } from "../common/date";
export default {
components: {
ClipPathEditor,
},
props: ["clipPathValue", "device"],
data() {
const clipPath = this.clipPathValue ? cloneDeep(this.clipPathValue) : undefined;
if (clipPath) {
for (const point of clipPath) {
point[0] = point[0] * .8 + 10;
point[1] = point[1] * .8 + 10;
}
}
return {
dateDialog: false,
adjustingTime: null,
startTime: null,
lastDetection: {},
objectListener: this.device.listen(
ScryptedInterface.ObjectDetector,
(s, d, e) => {
this.lastDetection = e || {};
}
),
muted: true,
sessionControl: undefined,
control: undefined,
clipPath,
};
},
computed: {
hasIntercom() {
return (
this.device.interfaces.includes(ScryptedInterface.Intercom) ||
this.device.providedInterfaces.includes(
ScryptedInterface.RTCSignalingChannel
)
);
},
isLive() {
return !this.startTime;
},
time() {
const d = this.startTime ? new Date(this.startTime) : new Date();
const h = d.getHours().toString().padStart(2, "0");
const m = d.getMinutes().toString().padStart(2, "0");
const s = d.getSeconds().toString().padStart(2, "0");
return `${h}:${m}:${s}`;
},
monthDay() {
const d = this.startTime ? new Date(this.startTime) : new Date();
return d.toLocaleDateString(undefined, {
month: "short",
day: "numeric",
});
},
showNvr() {
return this.device.interfaces.includes(ScryptedInterface.VideoRecorder);
},
svgWidth() {
return this.lastDetection?.inputDimensions?.[0] || 1920;
},
svgHeight() {
return this.lastDetection?.inputDimensions?.[1] || 1080;
},
svgContents() {
if (!this.lastDetection) return "";
let contents = "";
for (const detection of this.lastDetection.detections || []) {
if (!detection.boundingBox) continue;
const svgScale = this.svgWidth / 1080;
const sw = 2 * svgScale;
const s = "red";
const x = detection.boundingBox[0];
const y = detection.boundingBox[1];
const w = detection.boundingBox[2];
const h = detection.boundingBox[3];
let t = ``;
let toffset = 0;
if (detection.score && detection.className !== 'motion') {
t += `<tspan x='${x}' dy='${toffset}em'>${Math.round(detection.score * 100) / 100}</tspan>`
toffset -= 1.2;
}
const tname = detection.className + (detection.id ? `: ${detection.id}` : '')
t += `<tspan x='${x}' dy='${toffset}em'>${tname}</tspan>`
const fs = 20 * svgScale;
const box = `<rect x="${x}" y="${y}" width="${w}" height="${h}" stroke="${s}" stroke-width="${sw}" fill="none" />
<text x="${x}" y="${y - 5}" font-size="${fs}" dx="0.05em" dy="0.05em" fill="black">${t}</text>
<text x="${x}" y="${y - 5}" font-size="${fs}" fill="white">${t}</text>
`;
contents += box;
}
return contents;
},
},
mounted() {
this.streamCamera();
},
destroyed() {
this.cleanupConnection();
this.objectListener.removeListener();
},
methods: {
datePicked(value) {
this.dateDialog = false;
const dt = datePickerLocalTimeToUTC(value);
this.streamRecorder(dt);
},
doTimeScroll(e) {
if (!this.device.interfaces.includes(ScryptedInterface.VideoRecorder))
return;
if (!this.startTime) {
this.startTime = Date.now() - 2 * 60 * 1000;
return;
}
const adjust = Math.round(e.deltaY / 7);
this.startTime -= adjust * 60000;
clearTimeout(this.adjustingTime);
this.adjustingTime = setTimeout(() => {
this.adjustingTime = null;
this.streamRecorder(this.startTime);
}, 10);
},
cleanupConnection() {
console.log("control cleanup");
this.sessionControl?.close();
this.sessionControl = undefined;
},
async toggleMute() {
this.muted = !this.muted;
if (!this.sessionControl?.control) return;
this.sessionControl.control.setPlayback({
audio: !this.muted,
video: true,
});
this.sessionControl.session.setMicrophone(true);
},
async streamCamera() {
this.cleanupConnection();
this.startTime = null;
this.sessionControl = await streamCamera(
this.$scrypted.mediaManager,
this.device,
() => this.$refs.video
);
},
async streamRecorder(startTime) {
this.startTime = startTime;
const control = await streamRecorder(
this.$scrypted.mediaManager,
this.device,
startTime,
this.sessionControl?.recordingStream,
() => this.$refs.video
);
if (control) {
this.cleanupConnection();
this.sessionControl = control;
}
},
},
watch: {
clipPath() {
const clipPath = cloneDeep(this.clipPath);
for (const point of clipPath) {
point[0] = (point[0] - 10) / .8;
point[1] = (point[1] - 10) / .8;
}
this.$emit("clipPath", clipPath);
},
},
};
</script>
<style scoped>
.clip-path {
transform: scale(.8)
}
</style>