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/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-4ccde23d.js"></script>
9
- <link rel="stylesheet" href="/assets/index-442177d6.css">
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.1.9",
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
- "@types/jmuxer": "^2.0.3",
18
- "@vitejs/plugin-vue": "^4.0.0",
19
- "vite": "^4.0.0",
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
  }
@@ -8,9 +8,8 @@
8
8
  </v-container>
9
9
  <v-container>
10
10
  <v-row align="center" justify="center" style="height: 33vh;">
11
- <img v-if="visibility && image && !message" :src="image" class="h-100"/>
12
- <video v-show="visibility && !image && !message" id="player" autoplay muted playsinline class="h-100"></video>
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
- console.log("Connecting WebSocket");
48
- const wsurl = document.location.href.replace("http", "ws") + "/ws";
49
- this.ws = new WebSocket(wsurl);
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
  };