v4l2camera-ui 0.2.0 → 0.2.1

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.
package/dist/index.html CHANGED
@@ -5,8 +5,8 @@
5
5
  <link rel="icon" href="/favicon.png" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>v4l2camera</title>
8
- <script type="module" crossorigin src="/assets/index-H9K02A_M.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-Uptu2i-d.css">
8
+ <script type="module" crossorigin src="/assets/index-oUW9KiMz.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-DxZ7X0bX.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="app"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "v4l2camera-ui",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "serve": "vite --host=0.0.0.0 preview",
package/src/App.vue CHANGED
@@ -2,14 +2,14 @@
2
2
  <v-app>
3
3
  <h1>{{ msg }}</h1>
4
4
  <v-divider></v-divider>
5
- <v-content id="content">
5
+ <v-main id="content">
6
6
  <Video/>
7
7
  <v-divider></v-divider>
8
8
  <h3>Formats</h3>
9
9
  <Format/>
10
10
  <h3>Controls</h3>
11
11
  <Controls/>
12
- </v-content>
12
+ </v-main>
13
13
  <v-divider></v-divider>
14
14
  <v-footer>
15
15
  <v-container>
@@ -95,7 +95,15 @@
95
95
 
96
96
  <v-row v-if="format.audio">
97
97
  <v-col>Audio Format</v-col>
98
- <v-col cols="8">{{format.audio.format}}</v-col>
98
+ <v-col cols="8">
99
+ <v-select
100
+ v-model="format.audio.format"
101
+ :items="audioFormats"
102
+ :hint="format.description"
103
+ persistent-hint
104
+ @update:modelValue="updateValue(format)" >
105
+ </v-select>
106
+ </v-col>
99
107
  </v-row>
100
108
  <v-row v-if="format.audio">
101
109
  <v-col>Audio Sample Rate</v-col>
@@ -116,6 +124,7 @@ export default {
116
124
  axios.get("/api/formats").then(
117
125
  (response) => {
118
126
  this.formats = response.data.video;
127
+ this.audioFormats = response.data.audio;
119
128
 
120
129
  axios.get("/api/format").then(
121
130
  (response) => {
@@ -130,6 +139,7 @@ export default {
130
139
  return {
131
140
  formats: [],
132
141
  format: "",
142
+ audioFormats: [],
133
143
  geometry: ""
134
144
  };
135
145
  },
@@ -14,7 +14,25 @@
14
14
  </v-container>
15
15
  <v-container>
16
16
  <v-row align="center" justify="center">
17
- {{ this.rtspinfo?.url }}
17
+ {{ this.rtspinfo.url }}
18
+ </v-row>
19
+ </v-container>
20
+ <v-divider></v-divider>
21
+ <v-container>
22
+ <h3>RTSP</h3>
23
+ <v-row>
24
+ <v-col>RTSP Uri</v-col>
25
+ <v-col cols="8">
26
+ <v-text-field v-model="rtspinfo.rtspuri" @update:modelValue="updateRtspInfo()">
27
+ </v-text-field>
28
+ </v-col>
29
+ </v-row>
30
+ <v-row>
31
+ <v-col>Multicast Uri</v-col>
32
+ <v-col cols="8">
33
+ <v-text-field v-model="rtspinfo.multicasturi" @update:modelValue="updateRtspInfo()">
34
+ </v-text-field>
35
+ </v-col>
18
36
  </v-row>
19
37
  </v-container>
20
38
  </div>
@@ -29,9 +47,10 @@ export default {
29
47
  visibility: true,
30
48
  ws: null,
31
49
  message: null,
32
- rtspinfo: null,
50
+ rtspinfo: {rtspuri: '', multicasturi: '', url: ''},
33
51
  videoCanvas: null,
34
- format: null
52
+ format: {format: "", width: 0, height: 0},
53
+ frameResolved: null
35
54
  };
36
55
  },
37
56
  mounted() {
@@ -49,11 +68,16 @@ export default {
49
68
  );
50
69
  },
51
70
  created() {
52
- let wsurl = new URL("./ws", location.href);
71
+ let wsurl = new URL("./ws", import.meta.env.VITE_APP_BASE_URL || location.href);
53
72
  wsurl.protocol = wsurl.protocol.replace("http","ws");
54
73
  this.connectWebSocket(wsurl.href);
55
74
  },
56
75
  destroyed() {
76
+ console.log("Closing decoder");
77
+ if (this.ws.decoder) {
78
+ this.ws.decoder.close();
79
+ }
80
+ this.ws.decoder = null;
57
81
  console.log("Closing WebSocket");
58
82
  if (this.ws) {
59
83
  this.ws.close();
@@ -68,96 +92,98 @@ export default {
68
92
  axios.get("/api/stop").then( () => this.visibility = false);
69
93
  },
70
94
  async onMessage(message) {
71
- if (typeof message.data === 'string') {
72
- this.format = JSON.parse(message.data);
73
-
74
- } else if (message.data instanceof ArrayBuffer) {
75
- const bytes = new Uint8Array(message.data);
76
- if ( (bytes.length > 1) && (bytes[0] === 255) && (bytes[1] === 216)) {
77
- // JPEG
78
- let binaryStr = "";
79
- for (let i = 0; i < bytes.length; i++) {
80
- binaryStr += String.fromCharCode(bytes[i]);
81
- }
82
- this.message = null;
83
- const img = new Image();
84
- img.src = "data:image/jpeg;base64," + btoa(binaryStr);
85
- await new Promise(r => img.onload=r);
86
- this.videoCanvas.canvas.width = img.width;
87
- this.videoCanvas.canvas.height = img.height;
88
- this.videoCanvas.drawImage(img, 0, 0);
89
- if (this.ws.decoder) {
90
- this.ws.decoder.close();
91
- }
92
- this.ws.decoder = null;
93
- } else if ( (bytes.length > 3) && (bytes[0] === 0) && (bytes[1] === 0) && (bytes[2] === 0) && (bytes[3] === 1)) {
94
- // H264
95
- if (!this.ws.decoder) {
96
- this.ws.decoder = new VideoDecoder({
97
- output: (frame) => {
98
- this.videoCanvas.canvas.width = frame.displayWidth;
99
- this.videoCanvas.canvas.height = frame.displayHeight;
100
- this.videoCanvas.drawImage(frame, 0, 0);
101
- frame.close();
102
- },
103
- error: (e) => console.log(e.message),
104
- });
105
- }
95
+ const { data } = message;
96
+ if (typeof data === 'string') {
97
+ this.format = JSON.parse(data);
106
98
 
107
- const naluType = bytes[4] & 0x1F;
108
- if (this.ws.decoder.state !== "configured" && naluType === 7) {
109
- let codec = 'avc1.';
110
- for (let i = 0; i < 3; i++) {
111
- codec += ('00' + bytes[5+i].toString(16)).slice(-2);
112
- }
113
- const config = {codec};
114
- const support = await VideoDecoder.isConfigSupported(config);
115
- if (support.supported) {
116
- this.ws.decoder.configure(config);
117
- } else {
118
- this.message = `${codec} is not supported`;
119
- }
120
- }
121
- if (this.ws.decoder.state === "configured") {
122
- this.message = null;
123
- const chunk = new EncodedVideoChunk({
124
- timestamp: new Date().getTime(),
125
- type: (naluType === 7) || (naluType === 5) ? "key" : "delta",
126
- data: bytes,
127
- });
128
- this.ws.decoder.decode(chunk);
129
- }
130
- } else if (this.format) {
131
- try {
132
- const frame = new VideoFrame(bytes, {
133
- format: this.format.format,
134
- timestamp: performance.now(),
135
- codedWidth: this.format.width,
136
- codedHeight: this.format.height,
137
- });
138
- this.videoCanvas.canvas.width = frame.displayWidth;
139
- this.videoCanvas.canvas.height = frame.displayHeight;
140
- this.videoCanvas.drawImage(frame, 0, 0);
141
- frame.close();
142
- this.message = null;
143
- } catch (e) {
144
- this.message = `${this.format.format} is not supported`;
145
- }
146
-
147
- } else {
148
- this.message = 'format not supported';
149
- }
150
- }
99
+ } else if (data instanceof ArrayBuffer) {
100
+ const bytes = new Uint8Array(data);
101
+ try {
102
+ const frame = await this.onFrame(bytes);
103
+ this.displayFrame(frame);
104
+ this.message = null;
105
+ } catch (e) {
106
+ this.message = e;
107
+ }
108
+ }
151
109
  },
152
110
  connectWebSocket(wsurl) {
153
111
  console.log(`Connecting WebSocket to ${wsurl}`);
154
112
  this.ws = new WebSocket(wsurl);
155
113
  this.ws.binaryType = 'arraybuffer';
156
114
  this.ws.onmessage = this.onMessage;
157
- this.ws.onclose = () => {
158
- console.log(`WebSocket closed, reconnecting to ${wsurl}...`);
159
- setTimeout(() => this.connectWebSocket(wsurl), 1000);
160
- };
115
+ this.ws.onclose = () => setTimeout(() => this.connectWebSocket(wsurl), 1000);
116
+ },
117
+ displayFrame(frame) {
118
+ this.videoCanvas.canvas.width = frame.displayWidth;
119
+ this.videoCanvas.canvas.height = frame.displayHeight;
120
+ this.videoCanvas.drawImage(frame, 0, 0);
121
+ frame.close();
122
+ },
123
+ async onH264Frame(bytes) {
124
+ if (!this.ws.decoder) {
125
+ this.ws.decoder = new VideoDecoder({
126
+ output: (frame) => this.frameResolved(frame),
127
+ error: (e) => console.log(e.message),
128
+ });
129
+ }
130
+
131
+ const naluType = bytes[4] & 0x1F;
132
+ if (this.ws.decoder.state !== "configured" && naluType === 7) {
133
+ let codec = 'avc1.';
134
+ for (let i = 0; i < 3; i++) {
135
+ codec += ('00' + bytes[5+i].toString(16)).slice(-2);
136
+ }
137
+ const config = {codec};
138
+ const support = await VideoDecoder.isConfigSupported(config);
139
+ if (support.supported) {
140
+ this.ws.decoder.configure(config);
141
+ } else {
142
+ return Promise.reject(`${codec} is not supported`);
143
+ }
144
+ }
145
+ if (this.ws.decoder.state === "configured") {
146
+ const chunk = new EncodedVideoChunk({
147
+ timestamp: performance.now(),
148
+ type: "key",
149
+ data: bytes,
150
+ });
151
+ this.ws.decoder.decode(chunk);
152
+ return await new Promise(r => this.frameResolved = r);
153
+ } else {
154
+ return Promise.reject(`H264 decoder not configured`);
155
+ }
156
+ },
157
+ async onJPEGFrame(data) {
158
+ const decoder = new ImageDecoder({data, type: 'image/jpeg'});
159
+ const image = await decoder.decode();
160
+ return new VideoFrame(image.image, {timestamp: performance.now()});
161
+ },
162
+ onDefaultFrame(bytes) {
163
+ return new VideoFrame(bytes, {
164
+ format: this.format.format,
165
+ timestamp: performance.now(),
166
+ codedWidth: this.format.width,
167
+ codedHeight: this.format.height,
168
+ });
169
+ },
170
+ onFrame(bytes) {
171
+ if ( (bytes.length > 1) && (bytes[0] === 255) && (bytes[1] === 216)) {
172
+ return this.onJPEGFrame(bytes);
173
+
174
+ } else if ( (bytes.length > 3) && (bytes[0] === 0) && (bytes[1] === 0) && (bytes[2] === 0) && (bytes[3] === 1)) {
175
+ return this.onH264Frame(bytes);
176
+
177
+ } else if (this.format) {
178
+ return this.onDefaultFrame(bytes);
179
+
180
+ } else {
181
+ return Promise.reject(`Unknown format`);
182
+ }
183
+ },
184
+ async updateRtspInfo() {
185
+ const response = await axios.post("/api/rtspinfo", this.rtspinfo);
186
+ this.rtspinfo = response.data;
161
187
  }
162
188
  }
163
189
  };