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/assets/index-D_CdpxlP.js +26 -0
- package/dist/assets/index-tnC2SLkf.css +5 -0
- package/dist/index.html +2 -2
- package/package.json +2 -2
- package/src/App.vue +2 -2
- package/src/components/Format.vue +11 -1
- package/src/components/Video.vue +140 -87
- package/dist/assets/index-H9K02A_M.js +0 -26
- package/dist/assets/index-Uptu2i-d.css +0 -5
- /package/dist/assets/{materialdesignicons-webfont-e5j8FT_3.ttf → materialdesignicons-webfont-B7mPwVP_.ttf} +0 -0
- /package/dist/assets/{materialdesignicons-webfont-kq_ClZaA.eot → materialdesignicons-webfont-CSr8KVlo.eot} +0 -0
- /package/dist/assets/{materialdesignicons-webfont-6eb_lmTU.woff2 → materialdesignicons-webfont-Dp5v-WZN.woff2} +0 -0
- /package/dist/assets/{materialdesignicons-webfont-D15t_tsC.woff → materialdesignicons-webfont-PXm3-2wK.woff} +0 -0
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-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
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.
|
|
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
|
-
"
|
|
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-
|
|
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-
|
|
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">
|
|
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
|
},
|
package/src/components/Video.vue
CHANGED
|
@@ -14,7 +14,52 @@
|
|
|
14
14
|
</v-container>
|
|
15
15
|
<v-container>
|
|
16
16
|
<v-row align="center" justify="center">
|
|
17
|
-
{{ this.rtspinfo
|
|
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:
|
|
77
|
+
rtspinfo: {rtspuri: '', multicasturi: '', url: '', issrtp: false, issrtpencrypted: false, issrtsps: false},
|
|
33
78
|
videoCanvas: null,
|
|
34
|
-
format:
|
|
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
|
-
|
|
72
|
-
|
|
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
|
-
|
|
148
|
-
|
|
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
|
-
|
|
159
|
-
|
|
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
|
};
|