Skip to content

Commit cb4fa1d

Browse files
committed
Improve performance and memory use
* avoid accessing reactive properties in scanning process. This seems to put heavy pressure on memory. * pick lower "ideal" resoltion for cameras to make scanning frames less expensive. * reduce default scan interval so more frames can be scanned in less times.
1 parent b68657e commit cb4fa1d

File tree

3 files changed

+102
-78
lines changed

3 files changed

+102
-78
lines changed

src/components/QrcodeReader.vue

Lines changed: 29 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import Camera from '../misc/Camera.js'
1919
import isBoolean from 'lodash/isBoolean'
2020
2121
const NO_LOCATION = [] // use specific array instance to guarantee equality ([] !== [] but NO_LOCATION === NO_LOCATION)
22-
const SCAN_INTERVAL = 40 // milliseconds
2322
2423
export default {
2524
props: {
@@ -36,15 +35,7 @@ export default {
3635
3736
data () {
3837
return {
39-
// most recent decoded QR code content.
40-
// Is only null after unpause or before init, otherwise string.
41-
decodeResult: null,
42-
43-
// array of most recent detected QR code corner coordinates
44-
locateResult: NO_LOCATION,
45-
4638
camera: null,
47-
4839
destroyed: false,
4940
}
5041
},
@@ -67,10 +58,8 @@ export default {
6758
} else {
6859
withDefaults = {
6960
facingMode: { ideal: 'environment' },
70-
// width: { min: 360, ideal: 1280, max: 1920 },
71-
// height: { min: 240, ideal: 720, max: 1080 },
72-
width: { ideal: 360 },
73-
height: { ideal: 240 },
61+
width: { min: 360, ideal: 680, max: 1920 },
62+
height: { min: 240, ideal: 480, max: 1080 },
7463
...this.videoConstraints,
7564
}
7665
}
@@ -84,32 +73,6 @@ export default {
8473
},
8574
8675
watch: {
87-
/**
88-
* Propagate new decoding results to parent component. Since holding a
89-
* camera for a few seconds over a QR code, produces the same result
90-
* multiple times in a row, an event is only emitted when the result
91-
* value actually changes. To allow re-scanning after the component is
92-
* un-paused, decodeResult is set to null (see #8). Null values are never
93-
* emitted though.
94-
*/
95-
decodeResult (newValue) {
96-
if (newValue !== null) {
97-
this.$emit('decode', newValue)
98-
}
99-
},
100-
101-
/**
102-
* Propagates location of QR code when it changes. Just like with decoding
103-
* results, this event is only emitted when the detected location changes.
104-
*
105-
* While a QR code is actually in sight, position changes are detected
106-
* nearly each scanned frame anyway. While no QR code is detected though,
107-
* this makes sure that empty results are only emitted once.
108-
*/
109-
locateResult (newValue) {
110-
this.$emit('locate', newValue)
111-
},
112-
11376
/**
11477
* Automatically freezes the video stream when conditions for the scanning
11578
* process are not fullfilled anymore.
@@ -121,7 +84,7 @@ export default {
12184
shouldScan (shouldScan) {
12285
if (shouldScan) {
12386
this.$refs.video.play()
124-
this.keepScanning()
87+
this.startScanning()
12588
} else {
12689
this.$refs.video.pause()
12790
}
@@ -170,34 +133,30 @@ export default {
170133
this.camera = await Camera(this.constraints, this.$refs.video)
171134
},
172135
173-
/**
174-
* Continuously extracts frames from camera stream and tries to decode
175-
* potentially pictured QR codes.
176-
*/
177-
keepScanning () {
178-
if (this.shouldScan) {
179-
const imageData = this.camera.captureFrame()
180-
181-
window.requestAnimationFrame(() => {
182-
const result = Scanner.scan(imageData)
183-
184-
if (result !== null) {
185-
this.decodeResult = result.data
186-
187-
const locationArray = [
188-
result.location.topLeftCorner,
189-
result.location.topRightCorner,
190-
result.location.bottomRightCorner,
191-
result.location.bottomLeftCorner,
192-
]
193-
194-
this.locateResult = this.normalizeLocation(locationArray)
195-
} else {
196-
this.locateResult = NO_LOCATION
197-
}
198-
199-
window.setTimeout(this.keepScanning, SCAN_INTERVAL)
200-
})
136+
startScanning () {
137+
Scanner.keepScanning(this.camera, {
138+
decodeHandler: this.onDecode,
139+
locateHandler: this.onLocate,
140+
shouldContinue: () => this.shouldScan,
141+
})
142+
},
143+
144+
onDecode (content) {
145+
this.$emit('decode', content)
146+
},
147+
148+
onLocate (location) {
149+
if (location === null) {
150+
this.$emit('locate', NO_LOCATION)
151+
} else {
152+
const locationArray = this.normalizeLocation([
153+
location.topLeftCorner,
154+
location.topRightCorner,
155+
location.bottomRightCorner,
156+
location.bottomLeftCorner,
157+
])
158+
159+
this.$emit('locate', locationArray)
201160
}
202161
},
203162
@@ -207,10 +166,8 @@ export default {
207166
* the coordinates are re-calculated to be relative to the video element.
208167
*/
209168
normalizeLocation (locationArray) {
210-
const video = this.$refs.video
211-
212-
const widthRatio = video.offsetWidth / video.videoWidth
213-
const heightRatio = video.offsetHeight / video.videoHeight
169+
const widthRatio = this.camera.displayWidth / this.camera.resolutionWidth
170+
const heightRatio = this.camera.displayHeight / this.camera.resolutionHeight
214171
215172
return locationArray.map(({ x, y }) => ({
216173
x: x * widthRatio,

src/misc/Camera.js

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,38 @@ class Camera {
1111
this.stream = stream
1212
}
1313

14+
get resolutionWidth () {
15+
return this.videoEl.videoWidth
16+
}
17+
18+
get resolutionHeight () {
19+
return this.videoEl.videoHeight
20+
}
21+
22+
get displayWidth () {
23+
return this.videoEl.offsetWidth
24+
}
25+
26+
get displayHeight () {
27+
return this.videoEl.offsetHeight
28+
}
29+
1430
stop () {
1531
this.stream.getTracks().forEach(
1632
track => track.stop()
1733
)
1834
}
1935

2036
captureFrame () {
21-
const width = this.videoEl.videoWidth
22-
const height = this.videoEl.videoHeight
23-
24-
this.canvasCtx.drawImage(this.videoEl, 0, 0, width, height)
37+
this.canvasCtx.drawImage(
38+
this.videoEl, 0, 0,
39+
this.resolutionWidth,
40+
this.resolutionHeight
41+
)
2542

26-
return this.canvasCtx.getImageData(0, 0, width, height)
43+
return this.canvasCtx.getImageData(
44+
0, 0, this.resolutionWidth, this.resolutionHeight
45+
)
2746
}
2847

2948
}

src/misc/Scanner.js

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,53 @@ export function scan (imageData) {
44
const { data, width, height } = imageData
55
const result = jsQR(data, width, height)
66

7-
return result
7+
let content, location
8+
9+
if (result === null) {
10+
content = null
11+
location = null
12+
} else {
13+
content = result.data
14+
location = result.location
15+
}
16+
17+
return { content, location }
18+
}
19+
20+
/**
21+
* Continuously extracts frames from camera stream and tries to read
22+
* potentially pictured QR codes.
23+
*/
24+
export function keepScanning (camera, options) {
25+
const {
26+
decodeHandler,
27+
locateHandler,
28+
shouldContinue,
29+
scanInterval = 16, // milliseconds
30+
} = options
31+
32+
const recur = (contentBefore, locationBefore) => {
33+
return () => {
34+
const imageData = camera.captureFrame()
35+
const { content, location } = scan(imageData)
36+
37+
if (content !== null && content !== contentBefore) {
38+
decodeHandler(content)
39+
}
40+
41+
if (location !== locationBefore) {
42+
locateHandler(location)
43+
}
44+
45+
if (shouldContinue()) {
46+
window.setTimeout(() => {
47+
window.requestAnimationFrame(
48+
recur(content || contentBefore, location)
49+
)
50+
}, scanInterval)
51+
}
52+
}
53+
}
54+
55+
window.requestAnimationFrame(recur(null, null))
856
}

0 commit comments

Comments
 (0)