v4l2camera-ui 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ {
2
+ "recommendations": ["johnsoncodehk.volar", "johnsoncodehk.vscode-typescript-vue-plugin"]
3
+ }
package/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # v4l2camera
2
+
3
+ This template should help get you started developing with Vue 3 in Vite.
4
+
5
+ ## Recommended IDE Setup
6
+
7
+ [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.vscode-typescript-vue-plugin).
8
+
9
+ ## Customize configuration
10
+
11
+ See [Vite Configuration Reference](https://vitejs.dev/config/).
12
+
13
+ ## Project Setup
14
+
15
+ ```sh
16
+ npm install
17
+ ```
18
+
19
+ ### Compile and Hot-Reload for Development
20
+
21
+ ```sh
22
+ npm run dev
23
+ ```
24
+
25
+ ### Compile and Minify for Production
26
+
27
+ ```sh
28
+ npm run build
29
+ ```
package/index.html ADDED
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" href="/favicon.ico" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>v4l2camera</title>
8
+ </head>
9
+ <body>
10
+ <div id="app"></div>
11
+ <script type="module" src="/src/main.js"></script>
12
+ </body>
13
+ </html>
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "v4l2camera-ui",
3
+ "version": "0.0.0",
4
+ "scripts": {
5
+ "serve": "vite --host=0.0.0.0 preview",
6
+ "build": "vite build --emptyOutDir",
7
+ "dev": "vite --host=0.0.0.0"
8
+ },
9
+ "dependencies": {
10
+ "axios": "^1.0.0",
11
+ "jmuxer": "^2.0.5",
12
+ "vue": "^3.2.29",
13
+ "vuetify": "^3.0.0-beta.11"
14
+ },
15
+ "devDependencies": {
16
+ "@vitejs/plugin-vue": "^3.0.3",
17
+ "vite": "^3.1.0",
18
+ "vite-plugin-vuetify": "^1.0.0-alpha.12",
19
+ "vue-cli-plugin-vuetify": "~2.5.5"
20
+ }
21
+ }
Binary file
package/src/App.vue ADDED
@@ -0,0 +1,47 @@
1
+ <template>
2
+ <v-app>
3
+ <h1>{{ msg }}</h1>
4
+ <v-divider></v-divider>
5
+ <Video/>
6
+ <v-divider></v-divider>
7
+ <Format/>
8
+ <v-divider></v-divider>
9
+ <Controls/>
10
+ </v-app>
11
+ </template>
12
+
13
+ <script>
14
+ import Video from './components/Video.vue';
15
+ import Format from './components/Format.vue';
16
+ import Controls from './components/Controls.vue';
17
+ import axios from "axios";
18
+ import config from './config.js';
19
+
20
+ export default {
21
+ name: 'App',
22
+ components: {
23
+ Format,
24
+ Controls,
25
+ Video
26
+ },
27
+ mounted() {
28
+ axios({ method: "GET", url: config.serviceurl + "/api/capabilities" }).then(
29
+ (response) => this.msg = response.data.card
30
+ );
31
+ },
32
+ data() {
33
+ return {
34
+ msg: "..."
35
+ };
36
+ }
37
+ }
38
+ </script>
39
+
40
+ <style>
41
+ #app {
42
+ font-family: 'Avenir', Helvetica, Arial, sans-serif;
43
+ }
44
+ h1 {
45
+ text-align: center;
46
+ }
47
+ </style>
@@ -0,0 +1,74 @@
1
+ /* color palette from <https://github.com/vuejs/theme> */
2
+ :root {
3
+ --vt-c-white: #ffffff;
4
+ --vt-c-white-soft: #f8f8f8;
5
+ --vt-c-white-mute: #f2f2f2;
6
+
7
+ --vt-c-black: #181818;
8
+ --vt-c-black-soft: #222222;
9
+ --vt-c-black-mute: #282828;
10
+
11
+ --vt-c-indigo: #2c3e50;
12
+
13
+ --vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
14
+ --vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
15
+ --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
16
+ --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
17
+
18
+ --vt-c-text-light-1: var(--vt-c-indigo);
19
+ --vt-c-text-light-2: rgba(60, 60, 60, 0.66);
20
+ --vt-c-text-dark-1: var(--vt-c-white);
21
+ --vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
22
+ }
23
+
24
+ /* semantic color variables for this project */
25
+ :root {
26
+ --color-background: var(--vt-c-white);
27
+ --color-background-soft: var(--vt-c-white-soft);
28
+ --color-background-mute: var(--vt-c-white-mute);
29
+
30
+ --color-border: var(--vt-c-divider-light-2);
31
+ --color-border-hover: var(--vt-c-divider-light-1);
32
+
33
+ --color-heading: var(--vt-c-text-light-1);
34
+ --color-text: var(--vt-c-text-light-1);
35
+
36
+ --section-gap: 160px;
37
+ }
38
+
39
+ @media (prefers-color-scheme: dark) {
40
+ :root {
41
+ --color-background: var(--vt-c-black);
42
+ --color-background-soft: var(--vt-c-black-soft);
43
+ --color-background-mute: var(--vt-c-black-mute);
44
+
45
+ --color-border: var(--vt-c-divider-dark-2);
46
+ --color-border-hover: var(--vt-c-divider-dark-1);
47
+
48
+ --color-heading: var(--vt-c-text-dark-1);
49
+ --color-text: var(--vt-c-text-dark-2);
50
+ }
51
+ }
52
+
53
+ *,
54
+ *::before,
55
+ *::after {
56
+ box-sizing: border-box;
57
+ margin: 0;
58
+ position: relative;
59
+ font-weight: normal;
60
+ }
61
+
62
+ body {
63
+ min-height: 100vh;
64
+ color: var(--color-text);
65
+ background: var(--color-background);
66
+ transition: color 0.5s, background-color 0.5s;
67
+ line-height: 1.6;
68
+ font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
69
+ Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
70
+ font-size: 15px;
71
+ text-rendering: optimizeLegibility;
72
+ -webkit-font-smoothing: antialiased;
73
+ -moz-osx-font-smoothing: grayscale;
74
+ }
@@ -0,0 +1,78 @@
1
+ <template>
2
+ <v-container>
3
+ <v-row v-for="c in controls" :key="c.name">
4
+ <v-col>{{ c.name }}</v-col>
5
+ <v-col cols="8" v-if="c.menu">
6
+ <v-select :items="getItems(c.menu)"
7
+ v-model.number="c.value"
8
+ @update:modelValue="updateValue(c.id,c.value)">
9
+ </v-select>
10
+ </v-col>
11
+ <v-col cols="8" v-if="!c.menu && c.minimum == 0 && c.maximum == 1">
12
+ <v-switch
13
+ v-model.number="c.value"
14
+ color="blue"
15
+ :true-value="1"
16
+ :false-value="0"
17
+ @update:modelValue="updateValue(c.id,c.value)">
18
+ </v-switch>
19
+ </v-col>
20
+ <v-col cols="7" v-if="!c.menu && !(c.minimum == 0 && c.maximum == 1)">
21
+ <v-slider
22
+ v-model.number="c.value"
23
+ color="blue"
24
+ hide-spin-buttons
25
+ :min="c.minimum"
26
+ :max="c.maximum"
27
+ :step="c.step"
28
+ ticks="always"
29
+ :disabled="disabled(c.flags)"
30
+ @update:modelValue="updateValue(c.id,c.value)"
31
+ >
32
+ <template v-slot:prepend>{{c.minimum}}</template>
33
+ <template v-slot:append>{{c.maximum}}</template>
34
+ </v-slider>
35
+ </v-col>
36
+ <v-col cols="1" v-if="!c.menu && !(c.minimum == 0 && c.maximum == 1)">
37
+ <v-text-field
38
+ v-model.number="c.value"
39
+ @update:modelValue="updateValue(c.id,c.value)">
40
+ </v-text-field>
41
+ </v-col>
42
+ </v-row>
43
+ </v-container>
44
+ </template>
45
+
46
+ <script>
47
+ import axios from "axios";
48
+ import config from '../config.js';
49
+
50
+ export default {
51
+ mounted() {
52
+ axios.get(config.serviceurl + "/api/controls").then(
53
+ (response) => this.controls = response.data
54
+ );
55
+ },
56
+ data() {
57
+ return {
58
+ controls: [],
59
+ };
60
+ },
61
+ methods: {
62
+ updateValue(id, value) {
63
+ axios.post(config.serviceurl + "/api/control", {id, value} ).then(
64
+ (response) => this.controls.filter((d) => d.id == id).forEach((element) => element.value = response.data.value)
65
+ );
66
+ },
67
+ getItems(menu) {
68
+ return menu.map(item => ({"title": item.label, "value": item.value}));
69
+ },
70
+ disabled(flags) {
71
+ return (flags.find( (f) => f === "V4L2_CTRL_FLAG_INACTIVE") !== undefined);
72
+ }
73
+ }
74
+ };
75
+ </script>
76
+
77
+ <style scoped>
78
+ </style>
@@ -0,0 +1,151 @@
1
+ <template>
2
+ <v-container>
3
+ <v-row>
4
+ <v-col>Format</v-col>
5
+ <v-col cols="8">
6
+ <v-select
7
+ v-model="format.format"
8
+ :items="getFormats(formats)"
9
+ :hint="format.description"
10
+ persistent-hint
11
+ @update:modelValue="updateValue(format)" >
12
+ </v-select>
13
+ </v-col>
14
+ </v-row>
15
+
16
+ <v-row>
17
+ <v-col>Geometry</v-col>
18
+ <v-col cols="8" v-if="typeof format.frameSizes[0].width === 'number' && typeof format.frameSizes[0].height === 'number'">
19
+ <v-select v-model="geometry" :items="getGeometries(format.frameSizes)" @update:modelValue="updateGeometry()">
20
+ </v-select>
21
+ </v-col>
22
+ <v-col cols="7" v-if="typeof format.frameSizes[0].width === 'object' && typeof format.frameSizes[0].height === 'object'">
23
+ <v-slider
24
+ v-model.number="format.width"
25
+ :min="format.frameSizes[0].width.min"
26
+ :max="format.frameSizes[0].width.max"
27
+ :step="format.frameSizes[0].width.step"
28
+ color="blue"
29
+ @update:modelValue="updateValue(format)"
30
+ >
31
+ <template v-slot:prepend>{{format.frameSizes[0].width.min}}</template>
32
+ <template v-slot:append>{{format.frameSizes[0].width.max}}</template>
33
+ </v-slider>
34
+
35
+ <v-slider
36
+ v-model.number="format.height"
37
+ :min="format.frameSizes[0].height.min"
38
+ :max="format.frameSizes[0].height.max"
39
+ color="blue"
40
+ :step="format.frameSizes[0].height.step" @change="alert($event)"
41
+ @update:modelValue="updateValue(format)"
42
+ >
43
+ <template v-slot:prepend>{{format.frameSizes[0].height.min}}</template>
44
+ <template v-slot:append>{{format.frameSizes[0].height.max}}</template>
45
+ </v-slider>
46
+ </v-col>
47
+ <v-col cols="1" v-if="typeof format.frameSizes[0].width === 'object' && typeof format.frameSizes[0].height === 'object'">
48
+ <v-text-field
49
+ v-model.number="format.width"
50
+ @update:modelValue="updateValue(format)">
51
+ </v-text-field>
52
+ <v-text-field
53
+ v-model.number="format.height"
54
+ @update:modelValue="updateValue(format)">
55
+ </v-text-field>
56
+ </v-col>
57
+ </v-row>
58
+
59
+ <v-row>
60
+ <v-col>Fps</v-col>
61
+ <v-col cols="8" v-if="typeof format.frameSizes[0].intervals[0].fps === 'number'" >
62
+ <v-select v-model.number="format.fps" :items="getFps(format.frameSizes)">
63
+ </v-select>
64
+ </v-col>
65
+ <v-col cols="7" v-if="typeof format.frameSizes[0].intervals[0].fps === 'object'">
66
+ <v-slider
67
+ v-model.number="format.fps"
68
+ :min="format.frameSizes[0].intervals[0].fps.min"
69
+ :max="format.frameSizes[0].intervals[0].fps.max"
70
+ color="blue"
71
+ thumb-label="true"
72
+ :step="format.frameSizes[0].intervals[0].fps.step"
73
+ @update:modelValue="updateValue(format)"
74
+ >
75
+ <template v-slot:prepend>{{format.frameSizes[0].intervals[0].fps.min}}</template>
76
+ <template v-slot:append>{{format.frameSizes[0].intervals[0].fps.max}}</template>
77
+ </v-slider>
78
+ </v-col>
79
+ <v-col cols="1" v-if="typeof format.frameSizes[0].intervals[0].fps === 'object'">
80
+ <v-text-field
81
+ v-model.number="format.fps"
82
+ @update:modelValue="updateValue(format)">
83
+ </v-text-field>
84
+ </v-col>
85
+ </v-row>
86
+
87
+ </v-container>
88
+ </template>
89
+
90
+ <script>
91
+ import axios from 'axios';
92
+ import config from '../config.js';
93
+
94
+ export default {
95
+ mounted() {
96
+ axios({ method: "GET", url: config.serviceurl + "/api/formats" }).then(
97
+ (response) => {
98
+ this.formats = response.data;
99
+
100
+ axios({ method: "GET", url: config.serviceurl + "/api/format" }).then(
101
+ (response) => {
102
+ this.format = response.data;
103
+ this.updateFormatFields();
104
+ }
105
+ );
106
+ }
107
+ );
108
+ },
109
+ data() {
110
+ return {
111
+ formats: [],
112
+ format: "",
113
+ geometry: ""
114
+ };
115
+ },
116
+ methods: {
117
+ updateValue(format) {
118
+ axios.post(config.serviceurl + "/api/format", format ).then(
119
+ (response) => {
120
+ this.format = response.data;
121
+ this.updateFormatFields();
122
+ }
123
+ );
124
+ },
125
+ updateGeometry() {
126
+ console.log(this.geometry)
127
+ const sizes = this.geometry.split('x');
128
+ this.format.width = parseInt(sizes[0]);
129
+ this.format.height = parseInt(sizes[1]);
130
+ this.updateValue(this.format);
131
+ },
132
+ updateFormatFields() {
133
+ this.geometry = `${this.format.width}x${this.format.height}`;
134
+ this.format.description = this.formats.filter((d) => d.format == this.format.format)[0].description;
135
+ this.format.frameSizes = this.formats.filter((d) => d.format == this.format.format)[0].frameSizes;
136
+ },
137
+ getFormats(formats) {
138
+ return formats.map(item => item.format);
139
+ },
140
+ getGeometries(formats) {
141
+ return formats.sort((a,b) => a.width*a.height - b.width*b.height).map(item => `${item.width}x${item.height}`);
142
+ },
143
+ getFps(formats) {
144
+ return formats.filter((fmt) => fmt.width + 'x' + fmt.height == this.geometry)[0].intervals.map(f => f.fps);
145
+ }
146
+ }
147
+ };
148
+ </script>
149
+
150
+ <style scoped>
151
+ </style>
@@ -0,0 +1,95 @@
1
+ <template>
2
+ <div>
3
+ <v-container>
4
+ <v-row align="center" justify="space-around" >
5
+ <v-btn v-if="!visibility" v-on:click="start">Start</v-btn>
6
+ <v-btn v-if="visibility" v-on:click="stop">Stop</v-btn>
7
+ </v-row>
8
+ </v-container>
9
+ <v-container>
10
+ <v-row align="center" justify="center">
11
+ <img v-if="visibility && !message" :src="image" />
12
+ <video v-if="visibility && !image && !message" id="player" autoplay muted playsinline ></video>
13
+ <div v-if="message">{{this.message}}</div>
14
+ </v-row>
15
+ </v-container>
16
+ <v-container>
17
+ <v-row align="center" justify="center">
18
+ {{ this.rtspinfo?.url }}
19
+ </v-row>
20
+ </v-container>
21
+ </div>
22
+ </template>
23
+
24
+ <script>
25
+ import axios from "axios";
26
+ import JMuxer from 'jmuxer';
27
+ import config from '../config.js';
28
+
29
+ export default {
30
+ data() {
31
+ return {
32
+ image: "",
33
+ visibility: true,
34
+ ws: null,
35
+ message: null,
36
+ rtspinfo: null
37
+ };
38
+ },
39
+ mounted() {
40
+ axios({ method: "GET", url: config.serviceurl + "/api/rtspinfo" }).then(
41
+ (response) => this.rtspinfo = response.data
42
+ );
43
+ axios({ method: "GET", url: config.serviceurl + "/api/isCapturing" }).then(
44
+ (response) => this.visibility = response.data
45
+ );
46
+ },
47
+ created() {
48
+ console.log("Connecting WebSocket");
49
+ const wsurl = document.location.href.replace("http", "ws") + "/ws";
50
+ this.ws = new WebSocket(wsurl);
51
+ this.ws.binaryType = 'arraybuffer';
52
+ this.ws.onmessage = (message) => {
53
+ const bytes = new Uint8Array(message.data);
54
+ if ( (bytes.length > 1) && (bytes[0] === 255) && (bytes[1] === 216)) {
55
+ // JPEG
56
+ let binaryStr = "";
57
+ for (let i = 0; i < bytes.length; i++) {
58
+ binaryStr += String.fromCharCode(bytes[i]);
59
+ }
60
+ this.message = null;
61
+ this.image = "data:image/jpeg;base64," + btoa(binaryStr);
62
+ this.ws.jmuxer = null;
63
+ } else if ( (bytes.length > 3) && (bytes[0] === 0) && (bytes[1] === 0) && (bytes[2] === 0) && (bytes[3] === 1)) {
64
+ this.image = null;
65
+ this.message = null;
66
+ // H264
67
+ if (!this.ws.jmuxer) {
68
+ this.ws.jmuxer = new JMuxer({node: 'player', mode: 'video', readFpsFromTrack: true, flushingTime: 1000});
69
+ }
70
+ this.ws.jmuxer.feed({ video: bytes })
71
+ } else {
72
+ this.message = 'format not supported';
73
+ }
74
+ };
75
+ },
76
+ destroyed() {
77
+ console.log("Closing WebSocket");
78
+ if (this.ws) {
79
+ this.ws.close();
80
+ this.ws = null;
81
+ }
82
+ },
83
+ methods: {
84
+ start() {
85
+ axios.get(config.serviceurl + "/api/start").then( () => this.visibility = true);
86
+ },
87
+ stop() {
88
+ axios.get(config.serviceurl + "/api/stop").then( () => this.visibility = false);
89
+ }
90
+ }
91
+ };
92
+ </script>
93
+
94
+ <style>
95
+ </style>
package/src/config.js ADDED
@@ -0,0 +1,3 @@
1
+ export default {
2
+ serviceurl: ''
3
+ }
package/src/main.js ADDED
@@ -0,0 +1,10 @@
1
+ import 'vuetify/styles'
2
+ import { createApp } from 'vue'
3
+ import { createVuetify } from 'vuetify'
4
+ import App from './App.vue'
5
+
6
+ const vuetify = createVuetify();
7
+
8
+ createApp(App)
9
+ .use(vuetify)
10
+ .mount('#app')
package/vite.config.js ADDED
@@ -0,0 +1,25 @@
1
+ import { fileURLToPath, URL } from 'url'
2
+
3
+ import { defineConfig } from 'vite'
4
+ import vue from '@vitejs/plugin-vue'
5
+ import path from "path";
6
+
7
+
8
+ // https://github.com/vuetifyjs/vuetify-loader/tree/next/packages/vite-plugin
9
+ import vuetify from 'vite-plugin-vuetify'
10
+
11
+ // https://vitejs.dev/config/
12
+ export default defineConfig({
13
+ build: {
14
+ outDir: path.resolve(__dirname, '../webroot'),
15
+ },
16
+ plugins: [
17
+ vue(),
18
+ vuetify({ autoImport: true }),
19
+ ],
20
+ resolve: {
21
+ alias: {
22
+ '@': fileURLToPath(new URL('./src', import.meta.url))
23
+ }
24
+ }
25
+ })