v4l2camera-ui 0.1.9 → 0.2.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.
- package/dist/assets/index-H9K02A_M.js +26 -0
- package/dist/assets/index-Uptu2i-d.css +5 -0
- package/dist/assets/materialdesignicons-webfont-6eb_lmTU.woff2 +0 -0
- package/dist/assets/materialdesignicons-webfont-D15t_tsC.woff +0 -0
- package/dist/assets/{materialdesignicons-webfont-5159a347.eot → materialdesignicons-webfont-e5j8FT_3.ttf} +0 -0
- package/dist/assets/{materialdesignicons-webfont-be825c12.ttf → materialdesignicons-webfont-kq_ClZaA.eot} +0 -0
- package/dist/index.html +2 -3
- package/package.json +5 -6
- package/src/components/Video.vue +106 -36
- package/dist/assets/index-442177d6.css +0 -5
- package/dist/assets/index-4ccde23d.js +0 -11
- package/dist/assets/materialdesignicons-webfont-28c8f97f.woff +0 -0
- package/dist/assets/materialdesignicons-webfont-31010194.woff2 +0 -0
- /package/{vite.config.js → vite.config.ts} +0 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/index.html
CHANGED
|
@@ -5,11 +5,10 @@
|
|
|
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" href="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-H9K02A_M.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Uptu2i-d.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="app"></div>
|
|
13
|
-
|
|
14
13
|
</body>
|
|
15
14
|
</html>
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "v4l2camera-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
4
5
|
"scripts": {
|
|
5
6
|
"serve": "vite --host=0.0.0.0 preview",
|
|
6
7
|
"build": "vite build --emptyOutDir",
|
|
@@ -8,16 +9,14 @@
|
|
|
8
9
|
},
|
|
9
10
|
"dependencies": {
|
|
10
11
|
"axios": "^1.2.0",
|
|
11
|
-
"jmuxer": "^2.0.5",
|
|
12
12
|
"vue": "^3.2.29",
|
|
13
13
|
"vuetify": "^3.0.3"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
16
|
"@mdi/font": "^7.0.96",
|
|
17
|
-
"@
|
|
18
|
-
"
|
|
19
|
-
"vite": "^
|
|
20
|
-
"vite-plugin-vuetify": "^1.0.1",
|
|
17
|
+
"@vitejs/plugin-vue": "^5.0.3",
|
|
18
|
+
"vite": "^5.0.12",
|
|
19
|
+
"vite-plugin-vuetify": "^2.0.1",
|
|
21
20
|
"vue-cli-plugin-vuetify": "~2.5.5"
|
|
22
21
|
}
|
|
23
22
|
}
|
package/src/components/Video.vue
CHANGED
|
@@ -8,9 +8,8 @@
|
|
|
8
8
|
</v-container>
|
|
9
9
|
<v-container>
|
|
10
10
|
<v-row align="center" justify="center" style="height: 33vh;">
|
|
11
|
-
<
|
|
12
|
-
<
|
|
13
|
-
<div v-if="message" class="h-100">{{this.message}}</div>
|
|
11
|
+
<video v-show="visibility && !message" id="video" muted class="h-100" preload="none"></video>
|
|
12
|
+
<div v-if="message">{{this.message}}</div>
|
|
14
13
|
</v-row>
|
|
15
14
|
</v-container>
|
|
16
15
|
<v-container>
|
|
@@ -23,19 +22,25 @@
|
|
|
23
22
|
|
|
24
23
|
<script>
|
|
25
24
|
import axios from "axios";
|
|
26
|
-
import JMuxer from "jmuxer";
|
|
27
25
|
|
|
28
26
|
export default {
|
|
29
27
|
data() {
|
|
30
28
|
return {
|
|
31
|
-
image: "",
|
|
32
29
|
visibility: true,
|
|
33
30
|
ws: null,
|
|
34
31
|
message: null,
|
|
35
|
-
rtspinfo: null
|
|
32
|
+
rtspinfo: null,
|
|
33
|
+
videoCanvas: null,
|
|
34
|
+
format: null
|
|
36
35
|
};
|
|
37
36
|
},
|
|
38
37
|
mounted() {
|
|
38
|
+
const videoCanvas = document.createElement("canvas");
|
|
39
|
+
this.videoCanvas = videoCanvas.getContext("2d");
|
|
40
|
+
const video = document.getElementById("video");
|
|
41
|
+
video.srcObject = videoCanvas.captureStream();
|
|
42
|
+
video.play();
|
|
43
|
+
this.videoCanvas.clearRect(0,0,videoCanvas.width,videoCanvas.height);
|
|
39
44
|
axios.get("/api/rtspinfo").then(
|
|
40
45
|
(response) => this.rtspinfo = response.data
|
|
41
46
|
);
|
|
@@ -44,36 +49,9 @@ export default {
|
|
|
44
49
|
);
|
|
45
50
|
},
|
|
46
51
|
created() {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
this.
|
|
50
|
-
this.ws.binaryType = 'arraybuffer';
|
|
51
|
-
this.ws.onmessage = (message) => {
|
|
52
|
-
const bytes = new Uint8Array(message.data);
|
|
53
|
-
if ( (bytes.length > 1) && (bytes[0] === 255) && (bytes[1] === 216)) {
|
|
54
|
-
// JPEG
|
|
55
|
-
let binaryStr = "";
|
|
56
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
57
|
-
binaryStr += String.fromCharCode(bytes[i]);
|
|
58
|
-
}
|
|
59
|
-
this.message = null;
|
|
60
|
-
this.image = "data:image/jpeg;base64," + btoa(binaryStr);
|
|
61
|
-
if (this.ws.jmuxer) {
|
|
62
|
-
this.ws.jmuxer.destroy();
|
|
63
|
-
}
|
|
64
|
-
this.ws.jmuxer = null;
|
|
65
|
-
} else if ( (bytes.length > 3) && (bytes[0] === 0) && (bytes[1] === 0) && (bytes[2] === 0) && (bytes[3] === 1)) {
|
|
66
|
-
this.image = null;
|
|
67
|
-
this.message = null;
|
|
68
|
-
// H264
|
|
69
|
-
if (!this.ws.jmuxer) {
|
|
70
|
-
this.ws.jmuxer = new JMuxer({node: 'player', mode: 'video', readFpsFromTrack: true});
|
|
71
|
-
}
|
|
72
|
-
this.ws.jmuxer.feed({ video: bytes })
|
|
73
|
-
} else {
|
|
74
|
-
this.message = 'format not supported';
|
|
75
|
-
}
|
|
76
|
-
};
|
|
52
|
+
let wsurl = new URL("./ws", location.href);
|
|
53
|
+
wsurl.protocol = wsurl.protocol.replace("http","ws");
|
|
54
|
+
this.connectWebSocket(wsurl.href);
|
|
77
55
|
},
|
|
78
56
|
destroyed() {
|
|
79
57
|
console.log("Closing WebSocket");
|
|
@@ -88,6 +66,98 @@ export default {
|
|
|
88
66
|
},
|
|
89
67
|
stop() {
|
|
90
68
|
axios.get("/api/stop").then( () => this.visibility = false);
|
|
69
|
+
},
|
|
70
|
+
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
|
+
}
|
|
146
|
+
|
|
147
|
+
} else {
|
|
148
|
+
this.message = 'format not supported';
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
connectWebSocket(wsurl) {
|
|
153
|
+
console.log(`Connecting WebSocket to ${wsurl}`);
|
|
154
|
+
this.ws = new WebSocket(wsurl);
|
|
155
|
+
this.ws.binaryType = 'arraybuffer';
|
|
156
|
+
this.ws.onmessage = this.onMessage;
|
|
157
|
+
this.ws.onclose = () => {
|
|
158
|
+
console.log(`WebSocket closed, reconnecting to ${wsurl}...`);
|
|
159
|
+
setTimeout(() => this.connectWebSocket(wsurl), 1000);
|
|
160
|
+
};
|
|
91
161
|
}
|
|
92
162
|
}
|
|
93
163
|
};
|