@wemap/camera 3.2.5 → 3.2.8

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.
@@ -9,7 +9,8 @@
9
9
 
10
10
  <body>
11
11
  <a href="./simple.html">Simple</a><br />
12
- <a href="./camera-logger.html">Camera-logger</a>
12
+ <a href="./camera-logger.html">Camera-logger</a><br />
13
+ <a href="./qr-code-scanner.html">QR Code Scanner</a>
13
14
  </body>
14
15
 
15
16
  </html>
@@ -0,0 +1,115 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Camera</title>
8
+
9
+ <script src="/js/camera-lib.js"></script>
10
+
11
+ <style type="text/css">
12
+ html,
13
+ body {
14
+ height: 100%;
15
+ margin: 0px;
16
+ }
17
+
18
+ #camera-container {
19
+ width: 100%;
20
+ height: 100%;
21
+ }
22
+
23
+ #controls {
24
+ position: absolute;
25
+ bottom: 0;
26
+ z-index: 2;
27
+ margin: 5px;
28
+ }
29
+
30
+ #controls>div {
31
+ background: #ffffffaa;
32
+ padding-left: 10px;
33
+ padding-right: 10px;
34
+ min-width: 100px;
35
+ display: inline-block;
36
+ }
37
+
38
+ #scan-result {
39
+ position: absolute;
40
+ border: 1px solid black;
41
+ background: white;
42
+ top: 0;
43
+ width: 150px;
44
+ left: 50%;
45
+ transform: translate(-50%, 0%);
46
+ z-index: 2;
47
+ text-align: center;
48
+ line-height: 30px;
49
+ vertical-align: middle;
50
+ }
51
+ </style>
52
+ </head>
53
+
54
+ <body>
55
+
56
+ <div id="camera-container"></div>
57
+ <div id="scan-result">Waiting</div>
58
+ <div id="controls">
59
+ <div id="camera-handler">
60
+ <div class="title">Camera</div>
61
+ <button id="start-camera">Start</button>
62
+ <button id="stop-camera" disabled>Stop</button>
63
+ </div>
64
+ <div id="qrcode-handler">
65
+ <div class="title">Scanner</div>
66
+ <button id="start-scanner">Start</button>
67
+ <button id="stop-scanner" disabled>Stop</button>
68
+ </div>
69
+ </div>
70
+
71
+
72
+ <script>
73
+ /* global Camera, QrCodeScanner */
74
+
75
+ const cameraContainer = document.getElementById('camera-container');
76
+ const scanResultContainer = document.getElementById('scan-result');
77
+ const startCameraButton = document.getElementById('start-camera');
78
+ const stopCameraButton = document.getElementById('stop-camera');
79
+ const startScannerButton = document.getElementById('start-scanner');
80
+ const stopScannerButton = document.getElementById('stop-scanner');
81
+
82
+ const camera = new Camera(cameraContainer);
83
+ const qrCodeScanner = new QrCodeScanner(camera);
84
+ qrCodeScanner.on('scan', str => (scanResultContainer.innerHTML = str));
85
+
86
+
87
+ startCameraButton.addEventListener('click', () => {
88
+ camera.start();
89
+ startCameraButton.disabled = true;
90
+ stopCameraButton.disabled = false;
91
+ });
92
+
93
+ stopCameraButton.addEventListener('click', () => {
94
+ camera.stop();
95
+ startCameraButton.disabled = false;
96
+ stopCameraButton.disabled = true;
97
+ });
98
+
99
+
100
+ startScannerButton.addEventListener('click', () => {
101
+ qrCodeScanner.start();
102
+ startScannerButton.disabled = true;
103
+ stopScannerButton.disabled = false;
104
+ });
105
+
106
+ stopScannerButton.addEventListener('click', () => {
107
+ qrCodeScanner.stop();
108
+ startScannerButton.disabled = false;
109
+ stopScannerButton.disabled = true;
110
+ scanResultContainer.innerHTML = 'Waiting';
111
+ });
112
+ </script>
113
+ </body>
114
+
115
+ </html>
package/index.js CHANGED
@@ -1,3 +1,8 @@
1
+ import { BarcodeFormat } from '@zxing/library';
1
2
  import Camera from './src/Camera.js';
3
+ import QrCodeScanner from './src/QrCodeScanner.js';
4
+ import SharedCameras from './src/SharedCameras.js';
2
5
 
3
- export { Camera };
6
+ export {
7
+ BarcodeFormat, Camera, QrCodeScanner, SharedCameras
8
+ };
package/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "directory": "packages/camera"
12
12
  },
13
13
  "name": "@wemap/camera",
14
- "version": "3.2.5",
14
+ "version": "3.2.8",
15
15
  "bugs": {
16
16
  "url": "https://github.com/wemap/wemap-modules-js/issues"
17
17
  },
@@ -23,9 +23,9 @@
23
23
  ],
24
24
  "license": "ISC",
25
25
  "dependencies": {
26
- "@wemap/navigation-logger": "^3.2.5",
26
+ "@zxing/library": "^0.17.1",
27
27
  "events": "^3.1.0"
28
28
  },
29
29
  "type": "module",
30
- "gitHead": "e90b23e4265edc0073101a70c6300fa54cd3d379"
30
+ "gitHead": "fb5e61f42479a69332c2e3e6922bd15e4d3ee3f3"
31
31
  }
package/src/Camera.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import EventEmitter from 'events';
2
2
 
3
- import CameraLogger from './CameraLogger.js';
3
+ import SharedCameras from './SharedCameras.js';
4
4
 
5
5
  const GENERIC_HARDWARE_VERTICAL_FOV = 60;
6
6
 
@@ -31,16 +31,20 @@ class Camera extends EventEmitter {
31
31
 
32
32
  fov = null;
33
33
 
34
- _videoPlaying = false;
34
+ _isStarted = false;
35
35
 
36
36
  _hardwareVerticalFov = GENERIC_HARDWARE_VERTICAL_FOV;
37
37
  _resizeOnWindowChange;
38
38
 
39
- _cameraLogger;
40
-
39
+ /**
40
+ * @param {Node} container
41
+ * @param {boolean} resizeOnWindowChange
42
+ */
41
43
  constructor(container, resizeOnWindowChange = false) {
42
44
  super();
43
45
 
46
+ SharedCameras._add(this, container);
47
+
44
48
  this.videoContainer = container;
45
49
  this._resizeOnWindowChange = resizeOnWindowChange;
46
50
 
@@ -52,17 +56,15 @@ class Camera extends EventEmitter {
52
56
  this.videoElement.setAttribute('muted', '');
53
57
  this.videoElement.setAttribute('playsinline', '');
54
58
  this.videoContainer.appendChild(this.videoElement);
55
-
56
- this._cameraLogger = new CameraLogger();
57
59
  }
58
60
 
59
61
  async start() {
60
62
 
61
- if (this.videoPlaying) {
63
+ if (this._isStarted) {
62
64
  throw new Error('Camera is already started');
63
65
  }
64
66
 
65
- this.videoPlaying = true;
67
+ this._isStarted = true;
66
68
 
67
69
  await Camera.checkAvailability();
68
70
 
@@ -82,16 +84,19 @@ class Camera extends EventEmitter {
82
84
  window.addEventListener('resize', this.notifyContainerSizeChanged);
83
85
  }
84
86
 
85
- this._cameraLogger.attachStream(this.videoStream);
87
+ this.emit('start', {
88
+ videoElement: this.videoElement,
89
+ stream
90
+ });
86
91
  }
87
92
 
88
93
  async stop() {
89
94
 
90
- if (!this.videoPlaying) {
95
+ if (!this._isStarted) {
91
96
  throw new Error('Camera is not started yet');
92
97
  }
93
98
 
94
- this._cameraLogger.detachStream();
99
+ this.emit('stop');
95
100
 
96
101
  if (this.videoStream && this.videoStream.stop) {
97
102
  // compatibility with old JS API
@@ -106,7 +111,11 @@ class Camera extends EventEmitter {
106
111
  if (this._resizeOnWindowChange) {
107
112
  window.removeEventListener('resize', this.notifyContainerSizeChanged);
108
113
  }
109
- this.videoPlaying = false;
114
+ this._isStarted = false;
115
+ }
116
+
117
+ release() {
118
+ SharedCameras._remove(this);
110
119
  }
111
120
 
112
121
  static async checkAvailability(testUserMedia = false) {
@@ -138,6 +147,10 @@ class Camera extends EventEmitter {
138
147
  }
139
148
  }
140
149
 
150
+ get isStarted() {
151
+ return this._isStarted;
152
+ }
153
+
141
154
  set hardwareVerticalFov(hardwareVerticalFov) {
142
155
  this._hardwareVerticalFov = hardwareVerticalFov;
143
156
  this._computeFov();
@@ -209,7 +222,6 @@ class Camera extends EventEmitter {
209
222
  horizontal: fovH
210
223
  };
211
224
 
212
- this._cameraLogger.fov = this.fov;
213
225
  this.emit('fov.changed', this.fov);
214
226
  }
215
227
 
@@ -0,0 +1,149 @@
1
+ /* eslint-disable max-statements */
2
+ import EventEmitter from 'events';
3
+ import {
4
+ MultiFormatReader,
5
+ BarcodeFormat,
6
+ DecodeHintType,
7
+ RGBLuminanceSource,
8
+ BinaryBitmap,
9
+ HybridBinarizer
10
+ } from '@zxing/library';
11
+
12
+ import Camera from './Camera.js';
13
+
14
+ class QrCodeScanner extends EventEmitter {
15
+
16
+ static DEFAULT_FORMATS = [
17
+ BarcodeFormat.QR_CODE,
18
+ BarcodeFormat.DATA_MATRIX,
19
+ BarcodeFormat.CODABAR
20
+ ];
21
+
22
+ /**
23
+ * @type {Camera}
24
+ */
25
+ _camera;
26
+
27
+ /**
28
+ * @type {HTMLVideoElement}
29
+ */
30
+ _videoElement;
31
+
32
+ /**
33
+ * @type {HTMLCanvasElement}
34
+ */
35
+ _canvas;
36
+
37
+ /**
38
+ * @type {CanvasRenderingContext2D}
39
+ */
40
+ _ctx;
41
+
42
+ /**
43
+ * @type {MultiFormatReader}
44
+ */
45
+ _codeReader;
46
+
47
+ /**
48
+ * @type {number}
49
+ */
50
+ _intervalTime;
51
+
52
+ /**
53
+ * @type {number}
54
+ */
55
+ _intervalTick;
56
+
57
+ /**
58
+ * @type {boolean}
59
+ */
60
+ _isStarted = false;
61
+
62
+
63
+ /**
64
+ * @param {Camera} camera
65
+ */
66
+ constructor(camera) {
67
+ super();
68
+
69
+ this._camera = camera;
70
+ this._videoElement = camera.videoElement;
71
+
72
+ this._codeReader = new MultiFormatReader();
73
+
74
+ this._canvas = document.createElement('canvas');
75
+ this._ctx = this._canvas.getContext('2d');
76
+
77
+ this._camera.on('start', () => this._tryStart());
78
+ this._camera.on('stop', () => this._stop());
79
+ }
80
+
81
+ start(intervalTime = 500, formats = QrCodeScanner.DEFAULT_FORMATS) {
82
+ this._intervalTime = intervalTime;
83
+
84
+ if (this._isStarted) {
85
+ return;
86
+ }
87
+ this._isStarted = true;
88
+
89
+ const hints = new Map();
90
+ hints.set(DecodeHintType.POSSIBLE_FORMATS, formats);
91
+ this._codeReader.setHints(hints);
92
+
93
+ this._tryStart();
94
+ }
95
+
96
+ stop() {
97
+ this._isStarted = false;
98
+ this._stop();
99
+ }
100
+
101
+ _tryStart() {
102
+
103
+ if (!this._isStarted || !this._camera.isStarted) {
104
+ return;
105
+ }
106
+
107
+ this._width = this._videoElement.videoWidth;
108
+ this._height = this._videoElement.videoHeight;
109
+ this._canvas.width = this._width;
110
+ this._canvas.height = this._height;
111
+ this._luminances = new Uint8Array(this._width * this._height);
112
+
113
+ this._intervalTick = setInterval(() => this._onFrame(), this._intervalTime);
114
+ }
115
+
116
+ _stop() {
117
+ clearInterval(this._intervalTick);
118
+ }
119
+
120
+ _onFrame() {
121
+
122
+ if (!this._width || !this._height) {
123
+ return;
124
+ }
125
+
126
+ this._ctx.drawImage(this._videoElement, 0, 0, this._width, this._height);
127
+ const img = this._ctx.getImageData(0, 0, this._width, this._height);
128
+
129
+ for (let i = 0; i < this._luminances.length; i++) {
130
+ // eslint-disable-next-line no-bitwise
131
+ this._luminances[i] = ((img.data[i * 4] + img.data[i * 4 + 1] * 2 + img.data[i * 4 + 2]) / 4) & 0xFF;
132
+ }
133
+
134
+ const binaryBitmap = new BinaryBitmap(
135
+ new HybridBinarizer(
136
+ new RGBLuminanceSource(this._luminances, this._width, this._height)
137
+ )
138
+ );
139
+
140
+ try {
141
+ const result = this._codeReader.decodeWithState(binaryBitmap);
142
+ this.emit('scan', result.getText());
143
+ } catch (e) {
144
+ // do nothing
145
+ }
146
+ }
147
+ }
148
+
149
+ export default QrCodeScanner;
@@ -0,0 +1,57 @@
1
+ import { EventEmitter } from 'events';
2
+
3
+ import Camera from './Camera.js';
4
+
5
+ class SharedCameras extends EventEmitter {
6
+
7
+ /**
8
+ * Singleton pattern.
9
+ * @returns {SharedCameras}
10
+ */
11
+ static get instance() {
12
+ if (!this._instance) {
13
+ this._instance = new SharedCameras();
14
+ }
15
+ return this._instance;
16
+ }
17
+
18
+ _list = [];
19
+
20
+ /**
21
+ * @param {Camera} camera
22
+ * @param {Node}
23
+ */
24
+ _add(camera, container) {
25
+ const obj = {
26
+ camera,
27
+ container
28
+ };
29
+ this._list.push(obj);
30
+ this.emit('added', obj);
31
+ }
32
+
33
+ /**
34
+ * @param {Camera} camera
35
+ */
36
+ _remove(camera) {
37
+ this._list = this._list.filter(({ camera: _camera }) => _camera !== camera);
38
+ this.emit('removed', camera);
39
+ }
40
+
41
+ get list() {
42
+ return this._list;
43
+ }
44
+
45
+ getCameraByContainer(container) {
46
+ for (const {
47
+ camera, container: _container
48
+ } of this._list) {
49
+ if (container === _container) {
50
+ return camera;
51
+ }
52
+ }
53
+ return null;
54
+ }
55
+ }
56
+
57
+ export default SharedCameras.instance;
@@ -1,120 +0,0 @@
1
- import { NavigationLogger } from '@wemap/navigation-logger';
2
- import { TimeUtils } from '@wemap/utils';
3
-
4
- class CameraLogger {
5
-
6
- static _videoPart = 0;
7
-
8
- _videoStream = null;
9
-
10
- constructor() {
11
-
12
- NavigationLogger.on('started', () => {
13
- CameraLogger._videoPart = 0;
14
- if (this._videoStream !== null) {
15
- this._start();
16
- }
17
- });
18
-
19
- NavigationLogger.on('stopped', () => {
20
- this._stop();
21
- });
22
-
23
- }
24
-
25
- attachStream(videoStream) {
26
- this._videoStream = videoStream;
27
- if (NavigationLogger.isRecording) {
28
- this._start();
29
- }
30
- }
31
-
32
- detachStream() {
33
- this._videoStream = null;
34
- this._stop();
35
- }
36
-
37
- set fov(fov) {
38
- this._fov = fov;
39
- }
40
-
41
- _start() {
42
- CameraLogger._videoPart++;
43
-
44
- if (!('MediaRecorder' in window)) {
45
- return;
46
- }
47
-
48
- if (this.mediaRecorder) {
49
- throw new Error('CameraLogger is already recording');
50
- }
51
-
52
- const options = {
53
- mimeType: 'video/webm;codecs=vp8',
54
- bitsPerSecond: NavigationLogger.params.videoBitrate
55
- };
56
-
57
- let isFirstChunck = true;
58
-
59
- const startTime = TimeUtils.preciseTime;
60
-
61
- // https://stackoverflow.com/questions/56826079/how-to-concat-chunks-of-incoming-binary-into-video-webm-file-node-js
62
- this.mediaRecorder = new MediaRecorder(this._videoStream, options);
63
- this.mediaRecorder.ondataavailable = event => {
64
- if (event.data && event.data.size > 0) {
65
- if (isFirstChunck) {
66
- this._sendEvent('video-start', {
67
- // startTime: TimeUtils.preciseTime - NavigationLogger.params.flushInterval,
68
- startTime,
69
- part: CameraLogger._videoPart,
70
- fov: this._fov
71
- });
72
- isFirstChunck = false;
73
- }
74
- this._sendChunck(CameraLogger._videoPart, event.data);
75
- }
76
- };
77
-
78
- this.mediaRecorder.start(NavigationLogger.params.flushInterval);
79
- }
80
-
81
-
82
- _stop() {
83
-
84
- if (!this.mediaRecorder) {
85
- return;
86
- }
87
-
88
- this._sendEvent('video-end', {
89
- time: TimeUtils.preciseTime,
90
- part: CameraLogger._videoPart
91
- });
92
-
93
- this.mediaRecorder.stop();
94
- this.mediaRecorder = null;
95
- }
96
-
97
- _sendChunck(videoPart, chunck) {
98
- fetch(NavigationLogger.getVideoChunckUrl(videoPart), {
99
- method: 'POST',
100
- headers: { 'Content-Type': 'application/octet-stream' },
101
- body: chunck
102
- });
103
- }
104
-
105
- _sendEvent(eventName, data) {
106
- fetch(NavigationLogger.getVideoConfigUrl(), {
107
- method: 'POST',
108
- headers: {
109
- 'Accept': 'application/json',
110
- 'Content-Type': 'application/json'
111
- },
112
- body: JSON.stringify({
113
- eventName,
114
- data
115
- })
116
- });
117
- }
118
- }
119
-
120
- export default CameraLogger;