Skip to content

Commit f9f6e4c

Browse files
committed
Support Safari 12.1, fix canvas leak, and improve recognition
https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/srcObject On Safari 12 (and indeed later verisons of other browsers), the `video.src = URL.createObjectURL(mediaStream);` syntax is deprecated. Instead, it's been replaced by `video.srcObject`. However, using the srcObject syntax also lead to hundreds of individual canvases being created in Safari 12.1. This used lots of memory, slowing the app, and Safari then stopped the website from creating further canvasses and scanning failed to work. The longer the video preview was open the worse things became. This version of the code now uses a single canvas for rendering, rather than relying on garbage collection to clean them up over time. Additionally, Safari requires a new `playsinline` attribute for the display element in the html. Additionally, I found that on Safari 12.1 with an iPhone 6s plus, the generated image was vertically `squished`, and the bar-code wouldn't be recognised as it was not square. This changes the code to use the rendered window size for the canvas, rather than what the camera has been detected as. This seems to dramatically improve QR code recognition.
1 parent d54dc34 commit f9f6e4c

File tree

1 file changed

+53
-16
lines changed

1 file changed

+53
-16
lines changed

src/browser/src/createQRScannerInternal.js

+53-16
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ module.exports = function(){
2121
var thisScanCycle = null;
2222
var nextScan = null;
2323
var cancelNextScan = null;
24+
var snapshotCanvas = null;
25+
var snapshotCanvasContext = null;
2426

2527
// standard screen widths/heights, from 4k down to 320x240
2628
// widths and heights are each tested separately to account for screen rotation
@@ -282,7 +284,16 @@ module.exports = function(){
282284
}).then(function(mediaStream){
283285
activeMediaStream = mediaStream;
284286
var video = getVideoPreview();
285-
video.src = URL.createObjectURL(mediaStream);
287+
288+
// Newer browsers have deprecated `video.src`, so we attempt video.srcObject
289+
// first, and then fall back to video.src if that's not supported,
290+
// as per https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/srcObject#Supporting_fallback_to_the_src_property
291+
try {
292+
video.srcObject = mediaStream;
293+
} catch(error) {
294+
video.src = URL.createObjectURL(mediaStream);
295+
}
296+
286297
success(calcStatus());
287298
}, function(err){
288299
// something bad happened
@@ -292,38 +303,57 @@ module.exports = function(){
292303
});
293304
}
294305

295-
function getTempCanvasAndContext(videoElement){
296-
var tempCanvas = document.createElement('canvas');
297-
var camera = getCurrentCamera();
298-
tempCanvas.height = camera.height;
299-
tempCanvas.width = camera.width;
300-
var tempCanvasContext = tempCanvas.getContext('2d');
301-
tempCanvasContext.drawImage(videoElement, 0, 0, camera.width, camera.height);
302-
return {
303-
canvas: tempCanvas,
304-
context: tempCanvasContext
305-
};
306+
function updateSnapshotCanvas(videoElement){
307+
// Use the dimensions of the on-screen display, not the camera dimensions.
308+
//
309+
// Since we're snapshotting the content of the html element into an image,
310+
// we want the width/height to reflect the current rotation of the html
311+
// element. Without this, we can run into issues where the image is
312+
// vertically squashed.
313+
var width = videoElement.clientWidth;
314+
var height = videoElement.clientHeight;
315+
316+
// Force the canvas to match the underlying video presentation element,
317+
// to handle cases where the device has changed rotation since the last snapshot
318+
snapshotCanvas.width = width;
319+
snapshotCanvas.height = height;
320+
321+
snapshotCanvasContext.drawImage(videoElement, 0, 0, width, height);
306322
}
307323

308324
function getCurrentImageData(videoElement){
309-
var snapshot = getTempCanvasAndContext(videoElement);
310-
return snapshot.context.getImageData(0, 0, snapshot.canvas.width, snapshot.canvas.height);
325+
updateSnapshotCanvas(videoElement);
326+
327+
return snapshotCanvasContext.getImageData(0, 0, snapshotCanvas.width, snapshotCanvas.height);
311328
}
312329

313330
// take a screenshot of the video preview with a temp canvas
314331
function captureCurrentFrame(videoElement){
315-
return getTempCanvasAndContext(videoElement).canvas.toDataURL('image/png');
332+
updateSnapshotCanvas(videoElement);
333+
334+
return snapshotCanvas.toDataURL('image/png');
316335
}
317336

318337
function initialize(success, error){
319338
if(scanWorker === null){
320339
var workerBlob = new Blob([workerScript],{type: "text/javascript"});
321340
scanWorker = new Worker(URL.createObjectURL(workerBlob));
322341
}
342+
343+
// Create only one in-memory canvas, otherwise memory leaks can lead to
344+
// hundreds of canvases, which then causes the browser to run out of
345+
// canvas memory
346+
if(snapshotCanvas === null){
347+
snapshotCanvas = document.createElement('canvas');
348+
snapshotCanvasContext = snapshotCanvas.getContext('2d');
349+
}
350+
323351
if(!getVideoPreview()){
324352
// prepare DOM (sync)
325353
var videoPreview = document.createElement('video');
326354
videoPreview.setAttribute('autoplay', 'autoplay');
355+
videoPreview.setAttribute('playsinline', 'playsinline');
356+
videoPreview.setAttribute('muted', 'muted');
327357
videoPreview.setAttribute('id', ELEMENTS.preview);
328358
videoPreview.setAttribute('style', 'display:block;position:fixed;top:50%;left:50%;' +
329359
'width:auto;height:auto;min-width:100%;min-height:100%;z-index:' + ZINDEXES.preview +
@@ -408,6 +438,7 @@ module.exports = function(){
408438
var video = getVideoPreview();
409439
if(video){
410440
video.src = '';
441+
video.srcObject = '';
411442
}
412443
success(calcStatus());
413444
}
@@ -429,7 +460,13 @@ module.exports = function(){
429460
}
430461
};
431462
thisScanCycle = function(){
432-
scanWorker.postMessage(getCurrentImageData(video));
463+
imageData = getCurrentImageData(video);
464+
if (imageData){
465+
scanWorker.postMessage(imageData);
466+
} else {
467+
console.log("No imageData returned; have we run out of canvas memory?")
468+
}
469+
imageData = null;
433470
if(cancelNextScan !== null){
434471
// avoid race conditions, always clear before starting a cycle
435472
cancelNextScan();

0 commit comments

Comments
 (0)