cleanup multi webcam

Signed-off-by: Stefan Dej <meteyou@gmail.com>
This commit is contained in:
Stefan Dej 2021-03-28 22:58:27 +02:00
parent 85de19ffeb
commit cca33770fc
10 changed files with 470 additions and 519 deletions

14
package-lock.json generated
View File

@ -32,7 +32,7 @@
"vuex": "^3.6.2" "vuex": "^3.6.2"
}, },
"devDependencies": { "devDependencies": {
"@mdi/font": "^5.8.55", "@mdi/font": "^5.9.55",
"@vue/cli-plugin-babel": "^4.5.10", "@vue/cli-plugin-babel": "^4.5.10",
"@vue/cli-plugin-eslint": "^4.5.3", "@vue/cli-plugin-eslint": "^4.5.3",
"@vue/cli-plugin-pwa": "^4.5.10", "@vue/cli-plugin-pwa": "^4.5.10",
@ -1537,9 +1537,9 @@
"integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==" "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q=="
}, },
"node_modules/@mdi/font": { "node_modules/@mdi/font": {
"version": "5.8.55", "version": "5.9.55",
"resolved": "https://registry.npmjs.org/@mdi/font/-/font-5.8.55.tgz", "resolved": "https://registry.npmjs.org/@mdi/font/-/font-5.9.55.tgz",
"integrity": "sha512-8mrwfFBsmj+D67ZiGQSe5TU/lcWCtDyli2eshQ2fvLCZGRPqFMM23YQp4+JMOTpk5yMZKTeAwNWIYfITy76OHA==", "integrity": "sha512-jswRF6q3eq8NWpWiqct6q+6Fg/I7nUhrxYJfiEM8JJpap0wVJLQdbKtyS65GdlK7S7Ytnx3TTi/bmw+tBhkGmg==",
"dev": true, "dev": true,
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
@ -19950,9 +19950,9 @@
"integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==" "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q=="
}, },
"@mdi/font": { "@mdi/font": {
"version": "5.8.55", "version": "5.9.55",
"resolved": "https://registry.npmjs.org/@mdi/font/-/font-5.8.55.tgz", "resolved": "https://registry.npmjs.org/@mdi/font/-/font-5.9.55.tgz",
"integrity": "sha512-8mrwfFBsmj+D67ZiGQSe5TU/lcWCtDyli2eshQ2fvLCZGRPqFMM23YQp4+JMOTpk5yMZKTeAwNWIYfITy76OHA==", "integrity": "sha512-jswRF6q3eq8NWpWiqct6q+6Fg/I7nUhrxYJfiEM8JJpap0wVJLQdbKtyS65GdlK7S7Ytnx3TTi/bmw+tBhkGmg==",
"dev": true "dev": true
}, },
"@mrmlnc/readdir-enhanced": { "@mrmlnc/readdir-enhanced": {

View File

@ -33,7 +33,7 @@
"vuex": "^3.6.2" "vuex": "^3.6.2"
}, },
"devDependencies": { "devDependencies": {
"@mdi/font": "^5.8.55", "@mdi/font": "^5.9.55",
"@vue/cli-plugin-babel": "^4.5.10", "@vue/cli-plugin-babel": "^4.5.10",
"@vue/cli-plugin-eslint": "^4.5.3", "@vue/cli-plugin-eslint": "^4.5.3",
"@vue/cli-plugin-pwa": "^4.5.10", "@vue/cli-plugin-pwa": "^4.5.10",

View File

@ -21,7 +21,7 @@
<v-select <v-select
v-model="currentLanguage" v-model="currentLanguage"
:items="availableLanguages" :items="availableLanguages"
:label="$t('Settings.LanguagePanel.Language')" :label="$t('Settings.GeneralPanel.Language')"
hide-details hide-details
></v-select> ></v-select>
</v-col> </v-col>

View File

@ -3,9 +3,9 @@
<v-card> <v-card>
<v-toolbar flat dense> <v-toolbar flat dense>
<v-toolbar-title> <v-toolbar-title>
<span class="subheading" <span class="subheading">
><v-icon left>mdi-webcam</v-icon>{{ $t('Settings.WebcamPanel.Webcam') }}</span <v-icon left>mdi-webcam</v-icon>{{ $t('Settings.WebcamPanel.Webcams') }}
> </span>
</v-toolbar-title> </v-toolbar-title>
</v-toolbar> </v-toolbar>
<v-card-text class="py-3"> <v-card-text class="py-3">
@ -15,40 +15,35 @@
<v-switch <v-switch
v-model="boolNavi" v-model="boolNavi"
hide-details hide-details
label="Show in navigation" :label="$t('Settings.WebcamPanel.ShowInNavigation')"
class="mt-0" class="mt-0"
></v-switch> ></v-switch>
</v-col> </v-col>
</v-row> </v-row>
<v-row <v-row v-for="(webcam, index) in this.webcams" v-bind:key="index">
v-for="(webcam, index) in this.webcams" <v-col class="rounded transition-swing secondary py-2 px-2 mb-3" style="cursor: pointer;" >
v-bind:key="index"
>
<v-col
class="rounded transition-swing secondary py-2 px-2 mb-3"
style="cursor: pointer;"
>
<v-row align="center"> <v-row align="center">
<v-col class="px-4 mr-2 col-1"> <v-col class="px-4 mr-2 col-1">
<v-icon left>{{webcam.icon}}</v-icon> <v-icon left>{{ webcam.icon }}</v-icon>
</v-col> </v-col>
<v-col class="col-7"> <v-col class="col-7">
<strong>{{ webcam.name }}</strong> <strong>{{ webcam.name }}</strong>
</v-col> </v-col>
<v-col class="col-auto text-right" style="position:absolute;right:12px" <v-col class="col-auto text-right" style="position:absolute;right:12px">
><v-btn <v-btn
small small
class="minwidth-0 float-right" class="minwidth-0 float-right"
v-on:click.stop.prevent="editWebcam(webcam)" v-on:click.stop.prevent="editWebcam(webcam)"
><v-icon small>mdi-pencil</v-icon></v-btn
></v-col
> >
<v-icon small>mdi-pencil</v-icon>
</v-btn>
</v-col>
</v-row> </v-row>
</v-col> </v-col>
</v-row> </v-row>
<v-row> <v-row>
<v-col class="text-center mt-0"> <v-col class="text-center mt-0">
<v-btn @click="createWebcam">add webcam</v-btn> <v-btn @click="createWebcam">{{ $t("Settings.WebcamPanel.AddWebcam")}}</v-btn>
</v-col> </v-col>
</v-row> </v-row>
</v-container> </v-container>
@ -59,14 +54,13 @@
<v-toolbar flat dense color="primary"> <v-toolbar flat dense color="primary">
<v-toolbar-title> <v-toolbar-title>
<span class="subheading"> <span class="subheading">
<v-icon class="mdi mdi-webcam" left></v-icon> <v-icon class="mdi mdi-webcam" left></v-icon> {{ dialog.index === null ? $t("Settings.WebcamPanel.CreateWebcam") : $t("Settings.WebcamPanel.EditWebcam") }}
{{ dialog.index === null ? "Create" : "Edit" }} Webcam
</span> </span>
</v-toolbar-title> </v-toolbar-title>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn small class="minwidth-0" @click="dialog.bool = false" <v-btn small class="minwidth-0" @click="dialog.bool = false">
><v-icon small>mdi-close-thick</v-icon></v-btn <v-icon small>mdi-close-thick</v-icon>
> </v-btn>
</v-toolbar> </v-toolbar>
<v-card-text class="pt-3"> <v-card-text class="pt-3">
<v-container class="pb-0"> <v-container class="pb-0">
@ -79,12 +73,14 @@
<v-item-group> <v-item-group>
<v-menu :offset-y="true" title="Icon"> <v-menu :offset-y="true" title="Icon">
<template v-slot:activator="{ on, attrs }"> <template v-slot:activator="{ on, attrs }">
<v-btn class="px-2 minwidth-0" color="transparent" v-bind="attrs" v-on="on" elevation="0"><v-icon>{{getCurrentIcon()}}</v-icon></v-btn> <v-btn class="px-2 minwidth-0" color="transparent" v-bind="attrs" v-on="on" elevation="0">
<v-icon>{{ getCurrentIcon() }}</v-icon>
</v-btn>
</template> </template>
<v-list dense class="py-0"> <v-list dense class="py-0">
<v-list-item v-for="icon of this.iconItems" v-bind:key="icon.value" link @click="setCurrentIcon(icon.value)"> <v-list-item v-for="icon of this.iconItems" v-bind:key="icon.value" link @click="setCurrentIcon(icon.value)">
<v-list-item-icon class="mr-0"> <v-list-item-icon class="mr-0">
<v-icon small>{{icon.value}}</v-icon> <v-icon small>{{ icon.value }}</v-icon>
</v-list-item-icon> </v-list-item-icon>
<v-list-item-content> <v-list-item-content>
<v-list-item-title v-text="icon.text"></v-list-item-title> <v-list-item-title v-text="icon.text"></v-list-item-title>
@ -97,78 +93,72 @@
<v-col class="col-10"> <v-col class="col-10">
<v-text-field <v-text-field
v-model="dialog.name" v-model="dialog.name"
label="Name" :label="$t('Settings.WebcamPanel.Name')"
hide-details="auto" hide-details="auto"
:rules="[rules.required, rules.unique]" :rules="[rules.required, rules.unique]"
dense dense
></v-text-field> ></v-text-field>
</v-col> </v-col>
</v-row> </v-row>
<v-row class="mt-2 mx-0 mb-2" align="center"> <v-row>
<v-col class="py-1">
<v-text-field <v-text-field
v-model="dialog.config.url" v-model="dialog.url"
label="URL" :label="$t('Settings.WebcamPanel.WebcamURL')"
hide-details="auto" hide-details="auto"
:rules="[rules.required, rules.urlvalid]" :rules="[rules.required]"
></v-text-field> ></v-text-field>
</v-col>
</v-row> </v-row>
<v-row class="mt-2 mx-0 mb-2" align="center"> <v-row>
<v-col class="py-1">
<v-select <v-select
v-model="dialog.config.service" v-model="dialog.service"
:items="serviceItems" :items="serviceItems"
hide-details hide-details
label="Service" :label="$t('Settings.WebcamPanel.Service')"
class="mt-0"
></v-select> ></v-select>
</v-col>
</v-row> </v-row>
<v-row <v-row v-if="dialog.service === 'mjpegstreamer-adaptive'">
class="mt-2 mx-0 mb-2" <v-col class="py-1">
align="center"
v-if="dialog.config.service === 'mjpegstreamer-adaptive'"
>
<v-text-field <v-text-field
v-model="dialog.config.targetFps" v-model="dialog.targetFps"
hide-details hide-details
label="Target FPS" :label="$t('Settings.WebcamPanel.TargetFPS')"
class="mt-0"
></v-text-field> ></v-text-field>
</v-col>
</v-row> </v-row>
<v-row class="mt-2 mx-0 mb-2" align="center"> <v-row>
<v-col class="py-1">
<v-checkbox <v-checkbox
v-model="dialog.config.flipX" v-model="dialog.flipX"
hide-details hide-details
class="shrink mt-0" class="mt-1"
label="Flip webcam horizontally" :label="$t('Settings.WebcamPanel.FlipHorizontally')"
></v-checkbox> ></v-checkbox>
</v-col>
</v-row> </v-row>
<v-row class="mt-2 mx-0 mb-2" align="center"> <v-row>
<v-col class="py-1">
<v-checkbox <v-checkbox
v-model="dialog.config.flipY" v-model="dialog.flipY"
hide-details hide-details
class="shrink mt-0" class="mt-1"
label="Flip webcam vertically" :label="$t('Settings.WebcamPanel.FlipVertically')"
></v-checkbox> ></v-checkbox>
</v-col>
</v-row> </v-row>
</v-col> </v-col>
<v-col class="col-12 col-sm-6"> <v-col class="col-12 col-sm-6 text-center" align-self="center">
<v-row class="mt-2 mx-0 mb-2" align="center" style=" <vue-load-image v-if="['mjpegstreamer', 'mjpegstreamer-adaptive'].includes(dialog.service)">
height: 100%; <img slot="image" :src="dialog.url" alt="Preview" :style="webcamStyle" class="webcamImage" />
margin-top: auto!important; <v-progress-circular slot="preloader" indeterminate color="primary"></v-progress-circular>
margin-bottom: auto!important;"> <template slot="error">
<img <v-icon x-large>mdi-webcam-off</v-icon>
:src="dialog.config.url" <div class="subtitle-1 mt-2">{{ $t('Settings.WebcamPanel.UrlNotAvailable') }}</div>
class="webcamImage" </template>
:style="webcamStyle" </vue-load-image>
alt="Webcam"
v-if="
['mjpegstreamer', 'mjpegstreamer-adaptive'].includes(
dialog.config.service
)
"
/>
</v-row>
<v-row class="mt-2 mx-0 mb-2" align="center">
</v-row>
</v-col> </v-col>
</v-row> </v-row>
<v-row class="mt-3"> <v-row class="mt-3">
@ -188,7 +178,7 @@
:class="dialog.index !== null ? 'float-right' : ''" :class="dialog.index !== null ? 'float-right' : ''"
type="submit" type="submit"
> >
{{ dialog.index === null ? "save" : "update" }} webcam {{ dialog.index === null ? $t("Settings.WebcamPanel.SaveWebcam") : $t("Settings.WebcamPanel.UpdateWebcam") }}
</v-btn> </v-btn>
</v-col> </v-col>
</v-row> </v-row>
@ -202,11 +192,14 @@
</template> </template>
<script> <script>
import { mapGetters } from "vuex";
import VueLoadImage from "vue-load-image"
export default { export default {
components: {}, components: {
data: function() { 'vue-load-image': VueLoadImage
},
data: function () {
return { return {
dialog: { dialog: {
bool: false, bool: false,
@ -214,125 +207,113 @@ export default {
valid: false, valid: false,
name: "", name: "",
icon: "mdi-webcam", icon: "mdi-webcam",
config: { service: "mjpegstreamer-adaptiv",
service: "mjpegstreamer", targetFps: 15,
targetFps: 25,
url: "/webcam/?action=stream", url: "/webcam/?action=stream",
flipX: false, flipX: false,
flipY: false, flipY: false,
}, },
},
rules: { rules: {
required: (value) => value !== "" || "required", required: (value) => value !== "" || this.$t("Settings.WebcamPanel.Required"),
unique: (value) => unique: (value) => !this.existsWebcamName(value) || this.$t("Settings.WebcamPanel.NameAlreadyExists"),
!this.existsWebcamName(value) || "Name already exists",
}, },
iconItems: [ iconItems: [
{ value: "mdi-printer-3d", text: "Printer" }, { value: "mdi-printer-3d", text: this.$t("Settings.WebcamPanel.IconPrinter") },
{ value: "mdi-printer-3d-nozzle", text: "Nozzle" }, { value: "mdi-printer-3d-nozzle", text: this.$t("Settings.WebcamPanel.IconNozzle") },
{ value: "mdi-radiator-disabled", text: "Bed" }, { value: "mdi-radiator-disabled", text: this.$t("Settings.WebcamPanel.IconBed") },
{ value: "mdi-webcam", text: "Cam" }, { value: "mdi-webcam", text: this.$t("Settings.WebcamPanel.IconCam") },
{ value: "mdi-album", text: "Filament" }, { value: "mdi-album", text: this.$t("Settings.WebcamPanel.IconFilament") },
{ value: "mdi-door", text: "Door" }, { value: "mdi-door", text: this.$t("Settings.WebcamPanel.IconDoor") },
{ value: "mdi-raspberry-pi", text: "MCU" }, { value: "mdi-raspberry-pi", text: this.$t("Settings.WebcamPanel.IconMcu") },
{ value: "mdi-campfire", text: "Hot" }, { value: "mdi-campfire", text: this.$t("Settings.WebcamPanel.IconHot") },
], ],
serviceItems: [ serviceItems: [
{ value: "mjpegstreamer", text: "MJPEG-Streamer" }, { value: "mjpegstreamer", text: this.$t("Settings.WebcamPanel.Mjpegstreamer")},
{ { value: "mjpegstreamer-adaptive", text: this.$t("Settings.WebcamPanel.MjpegstreamerAdaptive") },
value: "mjpegstreamer-adaptive",
text: "Adaptive MJPEG-Streamer (experimental)",
},
], ],
}; };
}, },
computed: { computed: {
...mapGetters({ webcams: {
webcams: 'gui/getWebcams' get() {
}), return this.$store.getters["gui/getWebcams"]
},
},
boolNavi: { boolNavi: {
get() { get() {
return this.$store.state.gui.webcam.bool; return this.$store.state.gui.webcam.bool
}, },
set(showNav) { set(showNav) {
return this.$store.dispatch("gui/setSettings", { return this.$store.dispatch("gui/setSettings", {
webcam: { bool: showNav }, webcam: {bool: showNav},
}); })
}, },
}, },
webcamStyle() { webcamStyle() {
let transforms = ""; let transforms = "";
if (this.dialog.config.flipX) { if (this.dialog.flipX) transforms += " scaleX(-1)"
transforms += " scaleX(-1)"; if (this.dialog.flipY) transforms += " scaleY(-1)"
}
if (this.dialog.config.flipY) {
transforms += " scaleY(-1)";
}
if (transforms.trimLeft().length) { if (transforms.trimLeft().length) {
return { return { transform: transforms.trimLeft() }
transform: transforms.trimLeft(),
};
} }
return "";
return ""
}, },
}, },
mounted() { mounted() {
this.clearDialog(); this.clearDialog()
}, },
methods: { methods: {
getDefaultConfig(){ getCurrentIcon() {
return {
service: "mjpegstreamer",
targetFps: 25,
url: "/webcam/?action=stream",
flipX: false,
flipY: false,
}
},
getCurrentIcon(){
return this.dialog.icon return this.dialog.icon
}, },
setCurrentIcon(icon){ setCurrentIcon(icon) {
this.dialog.icon = icon this.dialog.icon = icon
}, },
existsWebcamName(name) { existsWebcamName(name) {
return ( return (this.webcams.findIndex(webcam => webcam.name === name && webcam.index !== this.dialog.index) !== -1)
this.webcams.findIndex(
(webcam) => webcam.name === name && webcam.index !== this.dialog.index
) >= 0
);
}, },
createWebcam() { createWebcam() {
this.clearDialog(); this.clearDialog()
this.dialog.bool = true;
this.dialog.bool = true
}, },
clearDialog() { clearDialog() {
this.dialog.bool = false; this.dialog.bool = false
this.dialog.index = null; this.dialog.index = null
this.dialog.name = ""; this.dialog.name = ""
this.dialog.icon = "mdi-webcam" this.dialog.icon = "mdi-webcam"
this.dialog.config = this.getDefaultConfig(); this.dialog.service = "mjpegstreamer-adaptive"
this.dialog.targetFps = 15
this.dialog.url = "/webcam/?action=stream"
this.dialog.flipX = false
this.dialog.flipY = false
}, },
editWebcam(webcam) { editWebcam(webcam) {
this.dialog.name = webcam.name; this.dialog.name = webcam.name
this.dialog.icon = webcam.icon; this.dialog.icon = webcam.icon
this.dialog.index = webcam.index; this.dialog.index = webcam.index
this.dialog.config = webcam.config; this.dialog.service = webcam.service
this.dialog.targetFps = webcam.targetFps
this.dialog.url = webcam.url
this.dialog.flipX = webcam.flipX
this.dialog.flipY = webcam.flipY
this.dialog.bool = true; this.dialog.bool = true
}, },
saveWebcam() { saveWebcam() {
if (this.dialog.valid) { if (this.dialog.valid) {
if (this.dialog.index) { if (this.dialog.index) this.$store.dispatch("gui/updateWebcam", this.dialog)
this.$store.dispatch("gui/updateWebcam", this.dialog); else this.$store.dispatch("gui/addWebcam", this.dialog)
} else this.$store.dispatch("gui/addWebcam", this.dialog);
this.clearDialog(); this.clearDialog()
} }
}, },
deleteWebcam() { deleteWebcam() {
this.$store.dispatch("gui/deleteWebcam", this.dialog); this.$store.dispatch("gui/deleteWebcam", this.dialog)
this.clearDialog();
this.clearDialog()
}, },
}, },
}; }
</script> </script>

View File

@ -10,19 +10,19 @@
<v-toolbar-title> <v-toolbar-title>
<span class="subheading"> <span class="subheading">
<v-icon left>mdi-webcam</v-icon> {{ $t('Panels.WebcamPanel.Webcam')}} <v-icon left>mdi-webcam</v-icon> {{ $t('Panels.WebcamPanel.Webcam')}}
<small v-if="this.webcamConfig.service === 'mjpegstreamer-adaptive' && this.time">( {{ $t('Panels.WebcamPanel.FPS')}}: {{ currentFPS }})</small> <small v-if="'service' in this.currentCam && this.currentCam.service === 'mjpegstreamer-adaptive' && this.time">( {{ $t('Panels.WebcamPanel.FPS')}}: {{ currentFPS }})</small>
</span> </span>
</v-toolbar-title> </v-toolbar-title>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-item-group v-if="this.webcams.length>=2"> <v-item-group v-if="this.webcams.length > 1">
<v-menu :offset-y="true" title="Webcam"> <v-menu :offset-y="true" title="Webcam">
<template v-slot:activator="{ on, attrs }"> <template v-slot:activator="{ on, attrs }">
<v-btn small class="px-2 minwidth-0" color="primary" v-bind="attrs" v-on="on">{{currentCam().name}} <v-icon small>mdi-menu-down</v-icon></v-btn> <v-btn small class="px-2 minwidth-0" color="primary" v-bind="attrs" v-on="on">{{ 'name' in currentCam ? currentCam.name : "unknown" }} <v-icon small>mdi-menu-down</v-icon></v-btn>
</template> </template>
<v-list dense class="py-0"> <v-list dense class="py-0">
<v-list-item v-for="webcam of this.webcams" v-bind:key="webcam.index" link @click="selectCam(webcam.name)" :disabled="checkSelectedCam(webcam.name)"> <v-list-item v-for="webcam of this.webcams" v-bind:key="webcam.index" link @click="currentCamName = webcam.name">
<v-list-item-icon class="mr-0"> <v-list-item-icon class="mr-0">
<v-icon small>{{webcam.icon}}</v-icon> <v-icon small>{{ webcam.icon }}</v-icon>
</v-list-item-icon> </v-list-item-icon>
<v-list-item-content> <v-list-item-content>
<v-list-item-title v-text="webcam.name"></v-list-item-title> <v-list-item-title v-text="webcam.name"></v-list-item-title>
@ -33,25 +33,30 @@
</v-item-group> </v-item-group>
</v-toolbar> </v-toolbar>
<v-card-text class="px-0 py-0 content"> <v-card-text class="px-0 py-0 content">
<img :src="url" class="webcamImage" :style="webcamStyle" @load="onLoad" alt="Webcam" v-if="streamValid()" @error="streamNotFound()"/> <template v-if="'service' in this.currentCam && this.currentCam.service === 'mjpegstreamer'">
<div style="width: 100%;padding-bottom: 56.75%!important;" v-if="!streamValid()"> <vue-load-image>
<div style="position:absolute;text-align:center;width:100%;top: 53%;transform: translateY(-50%);" v-if="this.webcams.length === 0"> <img slot="image" :src="url" alt="Preview" :style="webcamStyle" class="webcamImage" @load="onLoad" />
<span><v-icon size="100">mdi-camera-off</v-icon></span><br><br> <v-progress-circular slot="preloader" indeterminate color="primary"></v-progress-circular>
<span>Please configure a Webcam in the Settings!</span> <div slot="error" class="text-center py-5">
</div> <v-icon x-large>mdi-webcam-off</v-icon>
<div style="position:absolute;text-align:center;width:100%;top: 53%;transform: translateY(-50%);" v-if="this.webcams.length !== 0"> <div class="subtitle-1 mt-2">{{ $t('Settings.WebcamPanel.UrlNotAvailable') }}</div>
<span><v-icon size="100">mdi-camera-off</v-icon></span><br><br>
<span>Please check if the Stream URL is valid!</span>
</div>
</div> </div>
</vue-load-image>
</template>
<template v-else>
<img slot="image" :src="url" alt="Preview" :style="webcamStyle" class="webcamImage" @load="onLoad" />
</template>
</v-card-text> </v-card-text>
</v-card> </v-card>
</template> </template>
<script> <script>
import { mapGetters,mapState } from 'vuex' import VueLoadImage from "vue-load-image";
export default { export default {
components: {
'vue-load-image': VueLoadImage
},
data: function() { data: function() {
return { return {
validURL: true, validURL: true,
@ -70,120 +75,73 @@
}; };
}, },
created: function () { created: function () {
document.addEventListener("focus", () => this.handleRefreshChange(), false); document.addEventListener("focus", () => this.handleRefreshChange(), false)
document.addEventListener("visibilitychange", this.handleRefreshChange, false); document.addEventListener("visibilitychange", this.handleRefreshChange, false)
if(this.webcams.length>=1){
let currentWebcamConfig = this.currentCam().config
if(currentWebcamConfig.service === 'mjpegstreamer-adaptive') {
this.requestMjpeg()
}
}
},
components: {
}, },
computed: { computed: {
...mapState({ currentCamName: {
'webcamConfig': state => state.gui.webcam, get() {
}), let currentCamName = this.$store.state.gui.webcam.selectedCam
...mapGetters({ if (currentCamName !== undefined && this.webcams.findIndex(webcam => webcam.name === currentCamName) !== -1)
webcams: 'gui/getWebcams' return currentCamName
}),
if (this.webcams.length) return this.webcams[0].name
return ""
},
set(newVal) {
return this.$store.dispatch('gui/setSettings', { webcam: { selectedCam: newVal } })
}
},
currentCam() {
const currentCam = this.webcams.find(webcam => webcam.name === this.currentCamName)
if (currentCam) return currentCam
return {}
},
webcams: {
get() {
return this.$store.state.gui.webcam.configs
}
},
url() { url() {
if(this.webcams.length>=1){ if('url' in this.currentCam) {
let currentWebcamConfig = this.currentCam().config if ('service' in this.currentCam && this.currentCam.service === 'mjpegstreamer' && this.currentCam.url.indexOf("?") !== -1) {
if (currentWebcamConfig.service === 'mjpegstreamer' && currentWebcamConfig.url.indexOf("?") !== -1) { let basicUrl = this.currentCam.url
let basicUrl = currentWebcamConfig.url
const params = new URLSearchParams(basicUrl) const params = new URLSearchParams(basicUrl)
params.set('bypassCache', ""+this.refresh) params.set('bypassCache', ""+this.refresh)
return decodeURIComponent(params.toString()) return decodeURIComponent(params.toString())
} else if (currentWebcamConfig.service === 'mjpegstreamer-adaptive') { } else if ('service' in this.currentCam && this.currentCam.service === 'mjpegstreamer-adaptive') {
return this.imageData return this.imageData
} else {
return currentWebcamConfig.url
} }
return this.currentCam.url
} }
return "" return ""
}, },
webcamStyle() { webcamStyle() {
let transforms = ""; let transforms = ""
if (this.currentCam().config.flipX) { if ('flipX' in this.currentCam && this.currentCam.flipX) transforms += " scaleX(-1)"
transforms += " scaleX(-1)"; if ('flipX' in this.currentCam && this.currentCam.flipY) transforms += " scaleY(-1)"
} if (transforms.trimLeft().length) return { transform: transforms.trimLeft() }
if (this.currentCam().config.flipY) {
transforms += " scaleY(-1)"; return ""
}
if (transforms.trimLeft().length) {
return {
transform: transforms.trimLeft(),
};
}
return "";
}, },
}, },
methods: { methods: {
currentCam(){
if(this.webcams.length === 0){
return ""
}
let currentcam
for(let camindex = 0; camindex < this.webcams.length; camindex ++){
if(this.webcams[camindex].name === this.webcamConfig.selectedCam){
currentcam = this.webcams[camindex]
break
}
}
if(typeof(currentcam)==="undefined"){
this.selectCam(this.webcams[0].name)
return this.webcams[0]
}
return currentcam
},
streamNotFound(){
this.validURL=false;
},
streamValid(){
if(this.webcams.length === 0){
return false
}
if(['mjpegstreamer', 'mjpegstreamer-adaptive'].includes(this.currentCam().config.service)){
if(!this.validURL){
return false
}
return true
}
return false
},
streamAdaptiveMode(){
if(!this.streamValid()){
return false
}
if(this.currentCam().config.service === 'mjpegstreamer-adaptive' && this.time){
return true
}
return false
},
handleRefreshChange() { handleRefreshChange() {
if (!document.hidden) { if (!document.hidden) {
this.refresh = Math.ceil(Math.random() * Math.pow(10, 12)) this.refresh = Math.ceil(Math.random() * Math.pow(10, 12))
} }
}, },
selectCam(name){
this.validURL = true
this.$store.dispatch('gui/setSettings', { webcam: { selectedCam: name } })
},
checkSelectedCam(name){
return this.webcamConfig.selectedCam === name
},
onLoad() { onLoad() {
const end_time = performance.now() const end_time = performance.now()
const current_time = end_time - this.start_time const current_time = end_time - this.start_time
this.time = (this.time * this.time_smoothing) + (current_time * (1.0 - this.time_smoothing)) this.time = (this.time * this.time_smoothing) + (current_time * (1.0 - this.time_smoothing))
this.start_time = end_time this.start_time = end_time
const target_time = 1000/this.currentCam().config.targetFps const target_time = 1000 / this.currentCam.targetFps
const current_request_time = performance.now() - this.request_start_time const current_request_time = performance.now() - this.request_start_time
this.request_time = (this.request_time * this.request_time_smoothing) + (current_request_time * (1.0 - this.request_time_smoothing)) this.request_time = (this.request_time * this.request_time_smoothing) + (current_request_time * (1.0 - this.request_time_smoothing))
@ -194,18 +152,17 @@
}) })
}, },
requestMjpeg() { requestMjpeg() {
if(!this.isVisible) return if (!this.isVisible) return
if(!this.streamValid()) return if (!('url' in this.currentCam)) return
this.request_start_time = performance.now() this.request_start_time = performance.now()
let basicUrl = this.currentCam().config.url let basicUrl = this.currentCam.url
basicUrl = basicUrl.replace("action=stream", "action=snapshot") basicUrl = basicUrl.replace("action=stream", "action=snapshot")
if (basicUrl && basicUrl.indexOf("?") === -1) basicUrl += "?" if (basicUrl && basicUrl.indexOf("?") === -1) basicUrl += "?"
const params = new URLSearchParams(basicUrl) const params = new URLSearchParams(basicUrl)
params.set('bypassCache', ""+ this.refresh + (Math.random() * 1000)) params.set('bypassCache', ""+ this.refresh + (Math.random() * 1000))
this.currentFPS = Math.round(1000 / this.time) this.currentFPS = Math.round(1000 / this.time)
this.$nextTick(() => { this.$nextTick(() => {

View File

@ -336,6 +336,7 @@ export default {
GeneralPanel: { GeneralPanel: {
General: "General", General: "General",
PrinterName: "Printer Name", PrinterName: "Printer Name",
Language: "Language",
DisplayCANCEL_PRINT: "Display CANCEL_PRINT", DisplayCANCEL_PRINT: "Display CANCEL_PRINT",
DisplayZOffset: "Display Z-Offset-Panel", DisplayZOffset: "Display Z-Offset-Panel",
ShowCANCEL_PRINT: "Shows the CANCEL_PRINT button permanently - no second layer confirmation needed.", ShowCANCEL_PRINT: "Shows the CANCEL_PRINT button permanently - no second layer confirmation needed.",
@ -344,9 +345,6 @@ export default {
FactoryInfo: "Do you really want to reset mainsail to factory settings?", FactoryInfo: "Do you really want to reset mainsail to factory settings?",
ResetMainsail: "reset mainsail" ResetMainsail: "reset mainsail"
}, },
LanguagePanel: {
Language: "Language"
},
LimitsPanel: { LimitsPanel: {
MachineLimits: "Machine Limits", MachineLimits: "Machine Limits",
Velocity: "Velocity", Velocity: "Velocity",
@ -398,13 +396,36 @@ export default {
ERROR: "ERROR" ERROR: "ERROR"
}, },
WebcamPanel: { WebcamPanel: {
Webcam: "Webcam", Webcams: "Webcams",
WebcamURL: "Webcam URL",
Rotate: "Rotate",
FlipWebcamHorizontally: "Flip webcam horizontally",
FlipWebcamVertically: "Flip webcam vertically",
ShowInNavigation: "Show in navigation", ShowInNavigation: "Show in navigation",
Service: "Service" AddWebcam: "add webcam",
CreateWebcam: "Create Webcam",
EditWebcam: "Edit Webcam",
SaveWebcam: "Save Webcam",
UpdateWebcam: "Update Webcam",
IconPrinter: "Printer",
IconNozzle: "Nozzle",
IconBed: "Bed",
IconCam: "Cam",
IconFilament: "Filament",
IconDoor: "Door",
IconMcu: "MCU",
IconHot: "Hot",
Mjpegstreamer: "MJPEG-Streamer",
MjpegstreamerAdaptive: "Adaptive MJPEG-Streamer (experimental)",
Required: "required",
NameAlreadyExists: "Name already exists",
UrlNotAvailable: "URL not available",
Name: "Name",
WebcamURL: "Webcam URL",
Service: "Service",
TargetFPS: "Target FPS",
FlipHorizontally: "Flip webcam horizontally",
FlipVertically: "Flip webcam vertically",
} }
} }
} }

View File

@ -274,6 +274,7 @@ export default {
GeneralPanel: { GeneralPanel: {
General: "一般", General: "一般",
PrinterName: "打印机名称", PrinterName: "打印机名称",
Language: "语言",
DisplayCANCEL_PRINT: "显示 CANCEL_PRINT", DisplayCANCEL_PRINT: "显示 CANCEL_PRINT",
DisplayZOffset: "显示Z偏移面板", DisplayZOffset: "显示Z偏移面板",
ShowCANCEL_PRINT: "永久显示CANCEL_PRINT按钮-无需第二层确认。", ShowCANCEL_PRINT: "永久显示CANCEL_PRINT按钮-无需第二层确认。",
@ -282,9 +283,6 @@ export default {
FactoryInfo: "您是否真的要将主帆重置为出厂设置?", FactoryInfo: "您是否真的要将主帆重置为出厂设置?",
ResetMainsail: "重置主帆" ResetMainsail: "重置主帆"
}, },
LanguagePanel: {
Language: "语言"
},
LimitsPanel: { LimitsPanel: {
MachineLimits: "机器限制", MachineLimits: "机器限制",
Velocity: "速度", Velocity: "速度",

View File

@ -7,14 +7,24 @@ export default {
}, },
getData({ commit, dispatch, rootState }, payload) { getData({ commit, dispatch, rootState }, payload) {
let oldwebcamconfig = payload.value.webcam
// upgrade webcam config to multi webcam support
if ('webcam' in payload.value &&
(
'service' in payload.value.webcam ||
'targetFps' in payload.value.webcam ||
'url' in payload.value.webcam ||
'flipX' in payload.value.webcam ||
'flipY' in payload.value.webcam
)
) {
window.console.log("convert to multi webcam")
dispatch('upgradeWebcamConfig', payload.value.webcam)
delete payload.value.webcam
}
commit('setData', payload.value) commit('setData', payload.value)
if (typeof (oldwebcamconfig) !== "undefined") {
dispatch('convertCamConfig', oldwebcamconfig)
}
if ('tempchart' in payload.value && 'datasetSettings' in payload.value.tempchart) { if ('tempchart' in payload.value && 'datasetSettings' in payload.value.tempchart) {
commit('setTempchartDatasetSettings', payload.value.tempchart.datasetSettings) commit('setTempchartDatasetSettings', payload.value.tempchart.datasetSettings)
} }
@ -26,52 +36,6 @@ export default {
dispatch('printer/init', null, { root: true }) dispatch('printer/init', null, { root: true })
}, },
convertCamConfig({ dispatch }, payload) {
if (typeof (payload.configs) !== "undefined") {
return
}
let oldcamconfig = {
name: "Default",
icon: "mdi-webcam",
config: {
service: "mjpegstreamer",
targetFps: 25,
url: "/webcam/?action=stream",
flipX: false,
flipY: false,
},
}
let cleanupconfig = {
service: undefined,
targetFps: undefined,
url: undefined,
flipX: undefined,
flipY: undefined,
rotate: undefined,
rotateDegrees: undefined,
selectedCam: 'Default',
bool: payload.bool,
configs: [],
}
if (typeof (payload.url) !== "undefined") {
oldcamconfig.config.url = payload.url
}
if (typeof (payload.service) !== "undefined") {
oldcamconfig.config.service = payload.service
}
oldcamconfig.config.targetFps = payload.targetFps
oldcamconfig.config.flipX = payload.flipX
oldcamconfig.config.flipY = payload.flipY
cleanupconfig.configs.push(oldcamconfig)
dispatch('setSettings', { 'webcam': cleanupconfig })
},
setSettings({ commit }, payload) { setSettings({ commit }, payload) {
commit('setSettings', payload) commit('setSettings', payload)
@ -161,6 +125,25 @@ export default {
}) })
}, },
upgradeWebcamConfig({ state, dispatch }, payload) {
const webcam = {...state.webcam}
webcam.bool = payload.bool
webcam.configs[0] = {
name: 'Default',
icon: 'mdi-webcam',
service: 'service' in payload ? payload.service : "mjpegstreamer-adaptive",
targetFps: 'targetFps' in payload ? payload.targetFps : 15,
url: 'url' in payload ? payload.url : "/webcam/?action=stream",
flipX: 'flipX' in payload ? payload.flipX : false,
flipY: 'flipY' in payload ? payload.flipY : false,
}
dispatch('updateSettings', {
keyName: 'webcam',
newVal: webcam
})
},
setTempchartDatasetSetting({ commit, dispatch, state }, payload) { setTempchartDatasetSetting({ commit, dispatch, state }, payload) {
commit("setTempchartDatasetSetting", payload) commit("setTempchartDatasetSetting", payload)
dispatch('updateSettings', { dispatch('updateSettings', {

View File

@ -36,21 +36,17 @@ export function getDefaultState() {
} }
}, },
webcam: { webcam: {
selectedCam: "Default", selectedCam: "",
bool: false, bool: false,
configs: [ configs: [{
{
name: 'Default', name: 'Default',
icon: 'mdi-webcam', icon: 'mdi-webcam',
config: { service: "mjpegstreamer-adaptive",
service: "mjpegstreamer", targetFps: 15,
targetFps: 25,
url: "/webcam/?action=stream", url: "/webcam/?action=stream",
flipX: false, flipX: false,
flipY: false, flipY: false,
} }],
}
],
}, },
tempchart: { tempchart: {
intervalChartUpdate: 1000, intervalChartUpdate: 1000,

View File

@ -67,18 +67,33 @@ export default {
}, },
addWebcam(state, payload) { addWebcam(state, payload) {
state.webcam.configs.push({ const newWebcam = {
name: payload.name, name: payload.name,
icon: payload.icon, icon: payload.icon,
config: payload.config service: payload.service,
}) targetFps: payload.targetFps,
url: payload.url,
flipX: payload.flipX,
flipY: payload.flipY,
}
state.webcam.configs.push(newWebcam)
}, },
updateWebcam(state, payload) { updateWebcam(state, payload) {
if (state.webcam.configs[payload.index]) { if (state.webcam.configs[payload.index]) {
Vue.set(state.webcam.configs[payload.index], 'name', payload.name) const configs = state.webcam.configs
Vue.set(state.webcam.configs[payload.index], 'icon', payload.icon) configs[payload.index] = {
Vue.set(state.webcam.configs[payload.index], 'config', payload.config) name: payload.name,
icon: payload.icon,
service: payload.service,
targetFps: payload.targetFps,
url: payload.url,
flipX: payload.flipX,
flipY: payload.flipY,
}
Vue.set(state.webcam, 'configs', configs)
} }
}, },