Skip to content

Commit 8aa5eab

Browse files
committed
feat: preserve last frame when stream ends
Use additional canvas overlaying the camera stream to paint the last frame before pausing the component. In contrast to calling `video.pause()` the painted frame also persists when the stream is killed completely. Close #24
1 parent 7f4bf2a commit 8aa5eab

File tree

1 file changed

+90
-30
lines changed

1 file changed

+90
-30
lines changed

src/components/QrcodeReader.vue

Lines changed: 90 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@
1616
class="qrcode-reader__tracking-layer"
1717
></canvas>
1818

19-
<video
20-
ref="video"
21-
class="qrcode-reader__camera-layer"
22-
></video>
19+
<div class="qrcode-reader__camera-layer" ref="videoWrapper">
20+
<canvas ref="pauseFrame"></canvas>
21+
<video ref="video"></video>
22+
</div>
2323
</div>
2424
</div>
2525
</template>
@@ -28,10 +28,10 @@
2828
import * as Scanner from '../misc/scanner.js'
2929
import Camera from '../misc/camera.js'
3030
import { imageDataFromFile, imageDataFromUrl } from '../misc/image-data.js'
31-
import { hasFired } from '../misc/promisify.js'
3231
import isBoolean from 'lodash/isBoolean'
3332
3433
export default {
34+
3535
props: {
3636
paused: {
3737
type: Boolean,
@@ -60,7 +60,6 @@ export default {
6060
return {
6161
cameraInstance: null,
6262
destroyed: false,
63-
readyAfterPause: true,
6463
}
6564
},
6665
@@ -69,8 +68,7 @@ export default {
6968
shouldScan () {
7069
return this.paused === false &&
7170
this.cameraInstance !== null &&
72-
this.destroyed === false &&
73-
this.readyAfterPause
71+
this.destroyed === false
7472
},
7573
7674
/**
@@ -161,21 +159,11 @@ export default {
161159
}
162160
},
163161
164-
async paused (paused) {
165-
const video = this.$refs.video
166-
162+
paused (paused) {
167163
if (paused) {
168-
video.pause()
169-
170-
this.readyAfterPause = false
164+
this.stopPlayback()
171165
} else {
172-
this.repaintTrack(null)
173-
174-
video.play()
175-
176-
await hasFired(video, 'timeupdate')
177-
178-
this.readyAfterPause = true
166+
this.startPlayback()
179167
}
180168
},
181169
@@ -193,24 +181,20 @@ export default {
193181
},
194182
195183
beforeDestroy () {
196-
if (this.cameraInstance !== null) {
197-
this.cameraInstance.stop()
198-
}
199-
184+
this.beforeResetCamera()
200185
this.destroyed = true
201186
},
202187
203188
methods: {
204189
205190
async init () {
206-
if (this.cameraInstance !== null) {
207-
this.cameraInstance.stop()
208-
}
191+
this.beforeResetCamera()
209192
210-
if (this.videoConstraints === false) {
193+
if (this.constraints.video === false) {
211194
this.cameraInstance = null
212195
} else {
213196
this.cameraInstance = await Camera(this.constraints, this.$refs.video)
197+
this.startPlayback()
214198
}
215199
},
216200
@@ -223,6 +207,14 @@ export default {
223207
})
224208
},
225209
210+
beforeResetCamera () {
211+
if (this.cameraInstance !== null) {
212+
this.stopPlayback()
213+
this.cameraInstance.stop()
214+
this.cameraInstance = null
215+
}
216+
},
217+
226218
onLocate (location) {
227219
if (this.trackRepaintFunction !== null) {
228220
this.repaintTrack(location)
@@ -320,6 +312,64 @@ export default {
320312
})
321313
},
322314
315+
startPlayback () {
316+
this.unlockCameraLayerSize()
317+
this.repaintTrack(null)
318+
319+
const pauseFrame = this.$refs.pauseFrame
320+
const ctx = pauseFrame.getContext('2d')
321+
322+
ctx.clearRect(0, 0, pauseFrame.width, pauseFrame.height)
323+
},
324+
325+
stopPlayback () {
326+
this.lockCameraLayerSize()
327+
328+
const pauseFrame = this.$refs.pauseFrame
329+
const ctx = pauseFrame.getContext('2d')
330+
const cameraInstance = this.cameraInstance
331+
332+
const displayWidth = cameraInstance.displayWidth
333+
const displayHeight = cameraInstance.displayHeight
334+
335+
pauseFrame.width = displayWidth
336+
pauseFrame.height = displayHeight
337+
338+
ctx.drawImage(this.$refs.video, 0, 0, displayWidth, displayHeight)
339+
},
340+
341+
/*
342+
* When a new stream is requested, the video element looses its width and
343+
* height, causing the component to collapse until the new stream is loaded.
344+
* Copying the size from the video element to its wrapper div compensates
345+
* for this effect.
346+
*/
347+
lockCameraLayerSize () {
348+
const videoWrapper = this.$refs.videoWrapper
349+
const cameraInstance = this.cameraInstance
350+
351+
if (cameraInstance !== null) {
352+
console.log('TEST')
353+
354+
const displayWidth = cameraInstance.displayWidth
355+
const displayHeight = cameraInstance.displayHeight
356+
357+
videoWrapper.style.width = displayWidth + 'px'
358+
videoWrapper.style.height = displayHeight + 'px'
359+
}
360+
},
361+
362+
/**
363+
* The video elements wrapper div should not have a fixed size all the so
364+
* it can be responsive.
365+
*/
366+
unlockCameraLayerSize () {
367+
const videoWrapper = this.$refs.videoWrapper
368+
369+
videoWrapper.style.width = ''
370+
videoWrapper.style.height = ''
371+
},
372+
323373
},
324374
}
325375
</script>
@@ -337,14 +387,24 @@ export default {
337387
}
338388
339389
.qrcode-reader__camera-layer {
390+
position: relative;
391+
z-index: 10;
392+
}
393+
394+
.qrcode-reader__camera-layer > video {
340395
display: block;
341396
object-fit: contain;
397+
}
398+
399+
.qrcode-reader__inner-wrapper,
400+
.qrcode-reader__camera-layer,
401+
.qrcode-reader__camera-layer > video {
342402
max-width: 100%;
343403
max-height: 100%;
344-
z-index: 10;
345404
}
346405
347406
.qrcode-reader__overlay,
407+
.qrcode-reader__camera-layer > canvas,
348408
.qrcode-reader__tracking-layer {
349409
position: absolute;
350410
width: 100%;

0 commit comments

Comments
 (0)