v4l2camera-ui 0.2.0 → 0.2.1-10-g981908f

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-D_CdpxlP.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-tnC2SLkf.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="app"></div>
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "v4l2camera-ui",
3
- "version": "0.2.0",
3
+ "version": "0.2.1-10-g981908f",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "serve": "vite --host=0.0.0.0 preview",
7
7
  "build": "vite build --emptyOutDir",
8
- "dev": "vite --host=0.0.0.0"
8
+ "start": "vite --host=0.0.0.0"
9
9
  },
10
10
  "dependencies": {
11
11
  "axios": "^1.2.0",
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,52 @@
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>
36
+ </v-row>
37
+ <v-row>
38
+ <v-col>SRTP</v-col>
39
+ <v-col cols="8">
40
+ <v-switch
41
+ v-model.number="rtspinfo.issrtp" @update:modelValue="updateRtspInfo()"
42
+ color="blue">
43
+ </v-switch>
44
+ </v-col>
45
+ </v-row>
46
+ <v-row>
47
+ <v-col>SRTP encrypted</v-col>
48
+ <v-col cols="8">
49
+ <v-switch
50
+ v-model.number="rtspinfo.issrtpencrypted" @update:modelValue="updateRtspInfo()"
51
+ color="blue">
52
+ </v-switch>
53
+ </v-col>
54
+ </v-row>
55
+ <v-row>
56
+ <v-col>RTSPS</v-col>
57
+ <v-col cols="8">
58
+ <v-switch
59
+ v-model.number="rtspinfo.issrtsps" @update:modelValue="updateRtspInfo()"
60
+ color="blue">
61
+ </v-switch>
62
+ </v-col>
18
63
  </v-row>
19
64
  </v-container>
20
65
  </div>
@@ -29,9 +74,10 @@ export default {
29
74
  visibility: true,
30
75
  ws: null,
31
76
  message: null,
32
- rtspinfo: null,
77
+ rtspinfo: {rtspuri: '', multicasturi: '', url: '', issrtp: false, issrtpencrypted: false, issrtsps: false},
33
78
  videoCanvas: null,
34
- format: null
79
+ format: {format: "", width: 0, height: 0},
80
+ frameResolved: null
35
81
  };
36
82
  },
37
83
  mounted() {
@@ -49,11 +95,16 @@ export default {
49
95
  );
50
96
  },
51
97
  created() {
52
- let wsurl = new URL("./ws", location.href);
98
+ let wsurl = new URL("./ws", import.meta.env.VITE_APP_BASE_URL || location.href);
53
99
  wsurl.protocol = wsurl.protocol.replace("http","ws");
54
100
  this.connectWebSocket(wsurl.href);
55
101
  },
56
102
  destroyed() {
103
+ console.log("Closing decoder");
104
+ if (this.ws.decoder) {
105
+ this.ws.decoder.close();
106
+ }
107
+ this.ws.decoder = null;
57
108
  console.log("Closing WebSocket");
58
109
  if (this.ws) {
59
110
  this.ws.close();
@@ -68,96 +119,98 @@ export default {
68
119
  axios.get("/api/stop").then( () => this.visibility = false);
69
120
  },
70
121
  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
- }
106
-
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
- }
122
+ const { data } = message;
123
+ if (typeof data === 'string') {
124
+ this.format = JSON.parse(data);
146
125
 
147
- } else {
148
- this.message = 'format not supported';
149
- }
150
- }
126
+ } else if (data instanceof ArrayBuffer) {
127
+ const bytes = new Uint8Array(data);
128
+ try {
129
+ const frame = await this.onFrame(bytes);
130
+ this.displayFrame(frame);
131
+ this.message = null;
132
+ } catch (e) {
133
+ this.message = e;
134
+ }
135
+ }
151
136
  },
152
137
  connectWebSocket(wsurl) {
153
138
  console.log(`Connecting WebSocket to ${wsurl}`);
154
139
  this.ws = new WebSocket(wsurl);
155
140
  this.ws.binaryType = 'arraybuffer';
156
141
  this.ws.onmessage = this.onMessage;
157
- this.ws.onclose = () => {
158
- console.log(`WebSocket closed, reconnecting to ${wsurl}...`);
159
- setTimeout(() => this.connectWebSocket(wsurl), 1000);
160
- };
142
+ this.ws.onclose = () => setTimeout(() => this.connectWebSocket(wsurl), 1000);
143
+ },
144
+ displayFrame(frame) {
145
+ this.videoCanvas.canvas.width = frame.displayWidth;
146
+ this.videoCanvas.canvas.height = frame.displayHeight;
147
+ this.videoCanvas.drawImage(frame, 0, 0);
148
+ frame.close();
149
+ },
150
+ async onH264Frame(bytes) {
151
+ if (!this.ws.decoder) {
152
+ this.ws.decoder = new VideoDecoder({
153
+ output: (frame) => this.frameResolved(frame),
154
+ error: (e) => console.log(e.message),
155
+ });
156
+ }
157
+
158
+ const naluType = bytes[4] & 0x1F;
159
+ if (this.ws.decoder.state !== "configured" && naluType === 7) {
160
+ let codec = 'avc1.';
161
+ for (let i = 0; i < 3; i++) {
162
+ codec += ('00' + bytes[5+i].toString(16)).slice(-2);
163
+ }
164
+ const config = {codec};
165
+ const support = await VideoDecoder.isConfigSupported(config);
166
+ if (support.supported) {
167
+ this.ws.decoder.configure(config);
168
+ } else {
169
+ return Promise.reject(`${codec} is not supported`);
170
+ }
171
+ }
172
+ if (this.ws.decoder.state === "configured") {
173
+ const chunk = new EncodedVideoChunk({
174
+ timestamp: performance.now(),
175
+ type: "key",
176
+ data: bytes,
177
+ });
178
+ this.ws.decoder.decode(chunk);
179
+ return await new Promise(r => this.frameResolved = r);
180
+ } else {
181
+ return Promise.reject(`H264 decoder not configured`);
182
+ }
183
+ },
184
+ async onJPEGFrame(data) {
185
+ const decoder = new ImageDecoder({data, type: 'image/jpeg'});
186
+ const image = await decoder.decode();
187
+ return new VideoFrame(image.image, {timestamp: performance.now()});
188
+ },
189
+ onDefaultFrame(bytes) {
190
+ return new VideoFrame(bytes, {
191
+ format: this.format.format,
192
+ timestamp: performance.now(),
193
+ codedWidth: this.format.width,
194
+ codedHeight: this.format.height,
195
+ });
196
+ },
197
+ onFrame(bytes) {
198
+ if ( (bytes.length > 1) && (bytes[0] === 255) && (bytes[1] === 216)) {
199
+ return this.onJPEGFrame(bytes);
200
+
201
+ } else if ( (bytes.length > 3) && (bytes[0] === 0) && (bytes[1] === 0) && (bytes[2] === 0) && (bytes[3] === 1)) {
202
+ return this.onH264Frame(bytes);
203
+
204
+ } else if (this.format) {
205
+ return this.onDefaultFrame(bytes);
206
+
207
+ } else {
208
+ return Promise.reject(`Unknown format`);
209
+ }
210
+ },
211
+ async updateRtspInfo() {
212
+ const response = await axios.post("/api/rtspinfo", this.rtspinfo);
213
+ this.rtspinfo = response.data;
161
214
  }
162
215
  }
163
216
  };