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

16
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": {
@ -36489,4 +36489,4 @@
} }
} }
} }
} }

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

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