diff --git a/lib/frame-manager.js b/lib/frame-manager.js index f52c236d..d956bb49 100644 --- a/lib/frame-manager.js +++ b/lib/frame-manager.js @@ -3,6 +3,13 @@ const EventEmitter = require('events'); const util = require('util'); const RENDER_ELEMENT_ID = '__NIGHTMARE_RENDER__'; +const HIGHLIGHT_STYLE = { + x: 0, + y: 0, + width: 1, + height: 1, + color: {r: 0, g: 0, b: 0, a: 0.1} +}; module.exports = FrameManager; @@ -18,6 +25,7 @@ function FrameManager(window) { EventEmitter.call(this); var subscribed = false; var requestedFrame = false; + var frameRequestTimeout; var self = this; this.on('newListener', subscribe); @@ -40,6 +48,7 @@ function FrameManager(window) { function receiveFrame(buffer) { requestedFrame = false; + clearTimeout(frameRequestTimeout); self.emit('data', buffer); } @@ -47,39 +56,53 @@ function FrameManager(window) { * In addition to listening for events, calling `requestFrame` will ensure * that a frame is queued up to render (instead of just waiting for the next * time the browser chooses to draw a frame). - * @param {Function} [callback] Called when the frame is rendered. + * @param {Function} [callback] Called when the frame is rendered. + * @param {Number} [timeout=1000] If no frame has been rendered after this + many milliseconds, run the callback anyway. In this case, The + callback's first argument, an image buffer, will be `null`. */ - this.requestFrame = function(callback) { + this.requestFrame = function(callback, timeout) { + timeout = (timeout == undefined) ? 1000 : timeout; + if (callback) { this.once('data', callback); } + if (!requestedFrame) { - parent.emit('log', 'altering page to force rendering'); requestedFrame = true; - window.webContents.executeJavaScript( - '(' + triggerRender + ')("' + RENDER_ELEMENT_ID + '")'); + + // Force the browser to render new content by using the debugger to + // highlight a portion of the page. This way, we can guarantee a change + // that both requires rendering a frame and does not actually affect + // the content of the page. + if (!window.webContents.debugger.isAttached()) { + try { + window.webContents.debugger.attach(); + } + catch (error) { + parent.emit('log', `Failed to attach to debugger for frame subscriptions: ${error}`); + this.emit('data', null); + return; + } + } + + if (timeout) { + frameRequestTimeout = setTimeout(function() { + parent.emit('log', `FrameManager timing out after ${timeout} ms with no new rendered frames`); + self.emit('data', null) + }, timeout); + } + + parent.emit('log', 'Highlighting page to trigger rendering.'); + window.webContents.debugger.sendCommand('DOM.enable') + window.webContents.debugger.sendCommand( + 'DOM.highlightRect', HIGHLIGHT_STYLE, function(error) { + window.webContents.debugger.sendCommand('DOM.hideHighlight'); + window.webContents.debugger.detach(); + }); } }; }; util.inherits(FrameManager, EventEmitter); -// this runs in the render process and alters the render tree, forcing Chromium -// to draw a new frame. -var triggerRender = (function (id) { - var renderElement = document.getElementById(id); - if (renderElement) { - renderElement.remove(); - } - else { - renderElement = document.createElement('div'); - renderElement.id = id; - renderElement.setAttribute('style', - 'position: absolute;' + - 'left: 0;' + - 'top: 0;' + - 'width: 1px;' + - 'height: 1px;'); - document.documentElement.appendChild(renderElement); - } -}).toString();