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/assets/index-DxZ7X0bX.css +5 -0
- package/dist/assets/index-oUW9KiMz.js +26 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/App.vue +2 -2
- package/src/components/Format.vue +11 -1
- package/src/components/Video.vue +113 -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-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
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,25 @@
|
|
|
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>
|
|
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:
|
|
50
|
+
rtspinfo: {rtspuri: '', multicasturi: '', url: ''},
|
|
33
51
|
videoCanvas: null,
|
|
34
|
-
format:
|
|
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
|
-
|
|
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
|
-
}
|
|
95
|
+
const { data } = message;
|
|
96
|
+
if (typeof data === 'string') {
|
|
97
|
+
this.format = JSON.parse(data);
|
|
106
98
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
159
|
-
|
|
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
|
};
|