mirror of
https://github.com/koush/scrypted.git
synced 2026-05-02 12:40:25 +01:00
256 lines
8.0 KiB
Vue
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> {{ 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> |