I have a JavaScript widget that switches between image frames to create motion,
the widget fetches these frames when its render method is called, and discards reference to them when the dispose method is called.
The implementation works by using PreloadJS (http://www.createjs.com/#!/PreloadJS/documentation) to load a set of frames, it then renders the current frame to a <canvas>, or toggles display:none and display:block on the image elements on legacy browsers.
IE8 is causing issues because it doesn't want to free up the memory from these frames when the dispose method is called. (Note: This only happens if the images are in the DOM, otherwise everything is okay.)
The complete source can be found here: http://pastie.org/8061442
The images are added to the DOM on line 201:
var i = 0, img, node = self.$node[0];
for ( ; i < self.imageSet.length; i += 1 ) {
img = self.queue._loadedResults[self.imageSet[i]]
img.setAttribute("draggable", "false")
img.style.display = "none"
node.appendChild(img)
img = null
}
i = node = null
If this code is removed (and the accompanying disposal code here on :251-259) then the images are disposed correctly, it's only when the elements are added to the DOM that there is a problem.
If the render method is called after the dispose fails, the classic Stack verflow at line: 0 error gets thrown.
Any ideas / suggestions would be very helpful. Please look at the code before suggesting though, all references to the images has been nulled and the instance of the image loader has been disposed also, there are no event listeners on the image elements that would create a circular reference, etc. As far as I can see the images should be freed.
Related
We have a problem that is only evident on iOS browsers (iOS 12.0) with our SPA application that uses HTML object tags to load widgets (HTML/CSS/JS files) through JavaScript onto the page.
The issue is an intermittent one when the page is loaded some of the widgets don't display/render on the screen, yet are loaded into the DOM and can be viewed/highlighted with full element properties in the Safari Web Inspector. but are “invisible” to their user. The problem will occur about 50% of the time if there are 4 widgets to load on a page, 2 typically won't display and it will be different widgets not displaying each time, with no detectable pattern.
The widget javascript load events run properly and there are no errors in the console. In the Safari Web Inspector, we can see some of the HTML elements from the non-rendering object are loaded at position 0,0 but their style is correct in the DOM (left and top set correctly, display: inline, etc.).
Here is the code that loads the widgets (the fragment is added to the DOM after all widgets are setup):
function loadWidget(myFrag, widgetName) {
var widgetObj = document.createElement("object");
widgetObj.data = "widgets/" + widgets[widgetName].type + ".html"; // location of widget
widgetObj.className = "widget unselectable";
widgetObj.id = widgetName;
widgetObj.name = widgetName;
myFrag.appendChild(widgetObj); // build widgets onto fragment
widgetObj.addEventListener("load", widgetLoaded, false); // Run rest of widget initialisation after widget is in DOM
widgetObj.addEventListener("error", badLoad, true);
}
Here is the code in the load event that configures the widget once loaded (we work around a Chrome bug that also affects Safari where the load event is fired twice for every object loaded):
function widgetLoaded(e) {
var loadObj = e.target;
if (loadObj === null) {
// CHROME BUG: Events fire multiple times and error out early if widget file is missing so not loaded (but this still fires), force timeout
return;
}
var widgetName = loadObj.id;
// CHROME BUG: Workaround here is to just set the style to absolute so that the event will fire a second time and exit, then second time around run the entire widgetLoaded
if ((parent.g.isChrome || parent.g.isSafari) && !widgets[widgetName].loaded) {
widgets[widgetName].loaded = true; // CHROME: WidgetLoaded will get run twice due to bug, exit early first time.
loadObj.setAttribute("style", "position:absolute"); // Force a fake style to get it to fire again (without loading all the other stuff) and run through second time around
return;
}
var defView = loadObj.contentDocument.defaultView; // Pointer to functions/objects inside widget DOM
loadObj.setAttribute("style", "position:absolute;overflow:scroll;left:" + myWidget.locX + "px;top:" + myWidget.locY + "px;z-index:" + zIndex);
loadObj.width = myWidget.scaleX * defView.options.settings.iniWidth; // Set the width and height of the widget <object> in dashboard DOM
loadObj.height = myWidget.scaleY * defView.options.settings.iniHeight;
}
The code performs correctly in Chrome (Mac/Windows), IE and Safari (Mac), however, presents the random invisible loading issue in iOS Safari and also in iOS Chrome.
Any ideas what causes this and what the workaround could be?
We couldn't find the exact source of this issue after a lot of investigation and are fairly sure this is a webkit bug. However there is an acceptable workaround, which is to replace the object tag with an iframe tag, and it looks to be working exactly the same way (replace .data with .src) with a bonus it doesn't exhibit the chrome bug where onload events are fired twice, so Chrome runs our app faster now.
How do I grab a snapshot of a video file selected via <input type="file"> at a specific time in the video silently in-the-background (i.e. no visible elements, flickering, sound, etc.)?
There are four major steps:
Create <canvas> and <video> elements.
Load the src of the video file generated by URL.createObjectURL into the <video> element and wait for it to load by listening for specific events being fired.
Set the time of the video to the point where you want to take a snapshot and listen for additional events.
Use the canvas to grab the image.
Step 1 - Create the elements
This is very easy: just create one <canvas> and one <video> element and append them to <body> (or anywhere really, it doesn't really matter):
var canvasElem = $( '<canvas class="snapshot-generator"></canvas>' ).appendTo(document.body)[0];
var $video = $( '<video muted class="snapshot-generator"></video>' ).appendTo(document.body);
Notice that the video element has the attribute muted. Don't put any other attributes like autoplay or controls. Also notice that they both have the class snapshot-generator. This is so we can set the style for both of them so that they are out of the way:
.snapshot-generator {
display: block;
height: 1px;
left: 0;
object-fit: contain;
position: fixed;
top: 0;
width: 1px;
z-index: -1;
}
Some browsers work with them set to display: none, but other browsers will have serious problems unless they are rendered on the page, so we just make them minuscule so that they are essentially invisible. (Don't move them outside the viewport though, as otherwise you may see some ugly scrollbars on your page.)
Step 2 - Load the video
Here's where things start to get tricky. You need to listen to events to know when to continue. Different browsers will fire different events, different times and in different orders, so I'll save you the effort. There are three events that must always fire at least once before the video is ready; they are:
loadedmetadata
loadeddata
suspend
Set up the event handler for these events and keep track how many have fired. Once all three have fired, you are ready to proceed. Keep in mind that, since some of these events may fire more than once, you only want to handle the first event of each type that is fired, and discard subsequent firings. I used jQuery's .one, which takes care of this.
var step_2_events_fired = 0;
$video.one('loadedmetadata loadeddata suspend', function() {
if (++step_2_events_fired == 3) {
// Ready for next step
}
}).prop('src', insert_source_here);
The source should just be the object URL created via URL.createObjectURL(file), where file is the file object.
Step 3 - Set the time
This stage is similar to the previous: set the time and then listen for an event. Inside our if block from the previous code:
$video.one('seeked', function() {
// Ready for next step
}).prop('currentTime', insert_time_here_in_seconds);
Luckily its only one event this time, so it's pretty clear and concise. Finally...
Step 4 - Grab the snapshot
This part is just using the <canvas> element to grab a screenshot. Inside our seeked event handler:
canvas_elem.height = this.videoHeight;
canvas_elem.width = this.videoWidth;
canvas_elem.getContext('2d').drawImage(this, 0, 0);
var snapshot = canvas_elem.toDataURL();
// Remove elements as they are no longer needed
$video.remove();
$(canvas_elem).remove();
The canvas needs to match the dimensions of the video (not the <video> element) to get a proper image. Also, we are setting the canvas's internal .height and .width properties, not the canvas height/width CSS style values.
The value of snapshot is a data URI, which is basically just a string that starts with data:image/jpeg;base64 and then the base64 data.
Our final JS code should look like:
var step_2_events_fired = 0;
$video.one('loadedmetadata loadeddata suspend', function() {
if (++step_2_events_fired == 3) {
$video.one('seeked', function() {
canvas_elem.height = this.videoHeight;
canvas_elem.width = this.videoWidth;
canvas_elem.getContext('2d').drawImage(this, 0, 0);
var snapshot = canvas_elem.toDataURL();
// Delete the elements as they are no longer needed
$video.remove();
$(canvas_elem).remove();
}).prop('currentTime', insert_time_here_in_seconds);
}
}).prop('src', insert_source_here);
Celebrate!
You have your image in base64! Send this to your server, put it as the src of an <img> element, or whatever.
For example, you can decode it into binary and directly write it to a file (trim the prefix first), which will become a JPEG image file.
You could also use this to offer previews of videos while they are uploaded. If you are putting it as the src of an <img>, use the full data URI (don't remove the prefix).
Is it necessary to add an <img> to the DOM in order to preload it?
$(function whenDOMIsHappy(){
var $img = $('<img />')
.load(loadHandler)
.error(errorHandler)
.attr({src:"squatchordle-squashgarden.jpg"});
$img.appendTo('body .preloads'); // is this at all necessary?
});
// assuming <div class="preloads" style="display:none;"></div> exists in <body>.
I've seen mixed messages about this technique. I'm using jQuery, but the question applies to vanilla-people too.
I am interested in keeping this working in all major browsers.
All browsers I've tested do load images even if they're not in the DOM. You can test this with https://jsfiddle.net/84tu2s9p/.
const img = new Image();
img.src = "https://picsum.photos/200/300";
img.onload = () => console.log("loaded");
img.onerror = err => console.error(err);
Safari 13, 11.1, 10.1
Edge 18
Firefox 72, 70, 62
Chrome 78, 71
Opera 64
IE11
(Not meant to be an exhaustive list. I just tried a variety of versions and browsers.)
There's also the new image.decode() API that is intended for this use case of preloading images and avoids potential dropped frames when actually decoding the image to render it. Edge doesn't support it yet (Chromium-based Edge will though).
Given that HTML Canvas can use images without them being in the DOM, I think they have to load images.
as opposed to creating and then appending elements to the dom, why not just initialize a new image in javascript, then set its source to your images URL. this method should load your image without actually applying it to an element or rendering it on the dom - YET... take a peek:
someImageArray[0] = new Image();
someImageArray[0].src = "http://placehold.it/700x200/";
from here you are free to do what you wish with that image using javascript - render it directly in canvas or create an element out of it. however you might not even have to do anything. say if its already being referenced in other ajax based content. provided the URL is identical, the browser should use the cached version to draw the dom.
hope this helps here is a reference to a decent article about pre-loading with a few more options...
There is no guarantee that Images will be preloaded if you don't add it to the DOM! If you don't add it, the JavaScript Compiler can aggressively garbage collect the Image before it tries to load, because the element is not used at all.
This currently happens in firefox! There your images will not be preloaded if you don't add them to the DOM! - So be on the safe side and add them.
I have a slide show that has been working for a long time. I am updating the site to XHTML transitional, and now the slide show is not working in IE 9.
It seems the problem is that the "complete" function is not working. The following code gets the slide show started (this is called after the page loads):
function Initialize() {
document.images["carImg"].src = imgList[0];
if (document.getElementById) {
theLink = document.getElementById("linkTo");
theLink.href = imgURL[0];
}
if (document.images["carImg"].complete) SetTheInterval();
else setTimeout("Initialize()", 1000);
}
document.images["carImg"].complete always resolves to false, and so it calls Initialize every second. The image imgList[0] is loaded, because it is showing up. But the complete property is not being set.
If I comment out the if (document.images["carImg"].complete) conditional, and just call SetTheInterval(), the slide show works.
It also works in Firefox (with the conditional). It also works if I set IE 9 to "compatibility view" (though then other things look weird).
Does anyone know why the "complete" property is not getting set in IE 9? Has something changed?
UPDATE: It seems complete is only not working on the first image. For subsequent images, complete is set when the image is loaded.
Try seeing if there is an onload or an error event, it may be that even resetting the src makes a 'dirty' image that is not instantly complete, even if the image exists.
I hadn't noticed this before, but in IE10 (don't have 9) you need the onload event.
When the onload fires, complete is true, but as soon as you write to the img src it is false again. You could also set the src just once, and just check complete in the timeout.
And without the onload handler, even after the image appears, the timer goes on forever...
window.ready=function(){
var img= document.images[0];
img.onload= function(){
clearTimeout(ready.timer);
alert('onload called, img.complete= '+img.complete);
};
// temporary error handler, if needed
img.onerror=function(e){
clearTimeout(ready.timer);
alert('Error!\n'+e.message || e);
};
// set the new src
img.src= Memry.Rte+'art/sundrag.gif';
if(!img.complete) ready.timer=setTimeout(window.ready, 300);
else alert('complete');
}
ready()
I'm not sure this is the best answer, but time is a-wasting and it works.
The problem was, I had set the initial src for the image tag to spacer.gif (in HTML). Thus the complete property for the image was true when the Initialize routine was executed.
However, when I reset the src property -- in javascript -- to the first image in the image list, the complete property became false. I am not sure why. Subsequent to that, changing the src property to another image in the image list did set the complete property to true.
It was this way in both IE9 and Chrome, but not in Firefox. Go figure. So instead of setting the src of the image to spacer.gif, I read the javascript file containing the list of image names in the code-behind (C#) and set the initial src property of the image tag to the first image. Now it works.
FWIW, I did try using onerror in the image tag, but no error came up.
Sheesh. I am not fond of javascript.
I am having some frustrating javascript timing issues.
FYI, the page is a jsp file and attached to said page is a separate js file and the jQuery CDN file. For troubleshooting purposes, I eliminated all unnecessary content and code and pasted what I needed into separate jsp and js files to troubleshoot this specific problem.
If I could display the html and js someplace, that would be great. But for now, I'll describe it. The page has two buttons, one to load an image and one to toggle a "zoom" feature (more on that later). The user clicks a button, which loads an image using the DOM, specifically innerHTML. This image is surrounded by horizontal and vertical scrollbars. When the user turns on the "zoom" feature, the image records the mouse-click position in an onclick event. So, with this on, the user clicks on the image and a bigger version of the same image is loaded, again, using the DOM and innerHTML. The very last step, the most important one, using the mouse position, the scrollbars will focus and center on the point clicked (using scrollLeft and scrollTop).
This all works flawlessly in IE. However, in non-IE browers (i.e. FireFox), it takes a couple of clicks for the scroll adjustment to catch up to the innerHTML. That is, when the user "zooms" for the first time, the image loads but the scrollbars don't adjust. It takes two more successive clicks for it to work the same as in IE. I was researching innerHTML and it is slower in FireFox than IE.
How can I fix this? Has anybody else tried to load an image in FireFox using JavaScript and immediately adjust the scroll positioning on the image? Again, it works the first and each time after that in IE. But non-IE browsers are having issues.
I've tried using innerHTML, replaceChild, appendChild, nothing I tried so far fixes it.
Thank you.
Update: I wanted to see if this issue is anything inside the scrollbars or just images; so, I replaced the image with < p > ... < p > and programmed it to scroll immediately after the **first* image is loaded, via a user-initiated onclick event. Interestingly, it worked. I then replaced the text with the image and it was broken again.
So, after an image is loaded using the DOM (i.e. innerHTML), any attempts to programmatically scroll in non-IE browsers will break. If you programmatically scroll once more, though, it will behave normally.
Update2: I tried employing methods to programmatically cancel the event at the end of the call and immediately call the function again, but that didn't fix the issue.
Then, I tried loading the image using jquery and that seemed to work. I adapted it from two other stackoverflow articles: Can I get the image and load via ajax into div and img onload doesn't work well in IE7 (to circumvent a caching issue).
Here is the code I used:
image = new Image();
image.src = "sample.gif?d=" + new Date(); // passing unique url to fix IE (see link above)
image.onload = function () {
$("#imgcontainer").empty().append(image);
// document.getElementById("imgcontainer").appendChild(image); // This worked, too
// $("#imgcontainer").html("<img src=\"sample.gif?d=" + new Date() + "\"></img>"); // Failed
// document.getElementById("imgcontainer").innerHTML = "<img src=\"sample.gif?d" + new Date() + "\"></img>"; // Failed
$("#imgcontainer").scrollTop(25);
};
image.onerror = function () {
$("#imgcontainer").empty().html("That image is not available.");
}
$('#imgcontainer').empty().html('Loading...');
The key, I believe, was using the onload method. I tried employing jQuery.html() inside the onload method and it didn't work. So, that confirms there was definitely a timing issue related to innerHTML and how and when the image is loaded into the DOM. And the onload method, in combination with either the DOM's native appendChild method or jQuery's equivalent appendChild implementation, fixed the problem.
Update3:
Regarding mrtsherman's suggestion below--
Here is my code:
var outerDIV, innerDIV, html;
outerDIV = document.createElement("div");
outerDIV.id = "content";
document.getElementById("body_id").appendChild(outerDIV); // attach div to body
innerDIV = document.createElement("div");
innerDIV = "ImageHolder";
image = new Image();
image.src = "sample.gif?d=" + new Date();
document.getElementById("content").appendChild(innerDIV);
document.getElementById("ImageHolder").style.height=image.height + "px";
document.getElementById("ImageHolder").style.width=image.width + "px";
html = "<img src=\"sample.gif\"></img>";
$("#content").scrollTop(100);
$("#ImageHolder").html(html);
I created an inner div to place the image. After creating said div, I adjusted it's size, based on the dimensions of the image. I adjusted the scrolling in js and then attached the image to the DOM, via innerHTML, and it did not scroll. I changed the width and height to some fixed size larger than the image and it scrolled. But that is not the desired affect, to make a container bigger than the image. Thank you for your help.
Update4:
What is the equivalent of the code I wrote in Update2 when using document.createElement("img"), instead of new Image()? The onload event is not having the same affect as in the Image object; the onload event is an important ingredient, here.
Thank you.
If you know the new images dimensions then I would send those first. Then you can resize a container for the image, adjust scrollbars and then get the image.
Please see the original question, Update2, for the solution I employed.