Skip to content

Commit

Permalink
screenshoter
Browse files Browse the repository at this point in the history
  • Loading branch information
ebidel committed Sep 14, 2012
1 parent e73c169 commit 5d54fc7
Show file tree
Hide file tree
Showing 6 changed files with 357 additions and 0 deletions.
23 changes: 23 additions & 0 deletions screenshoter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Instructions

- Install the ws websocket library for Node: [http://einaros.github.com/ws/](http://einaros.github.com/ws/)

- Start a webserver:

python -m SimpleHTTPServer 8000

- Start the websocket server:

node server.js --port=3000

- Open your browser to [http://localhost:8000](http://localhost:8000).

- Save the contents of `bookmarklet.js` as a bookmark. For convenience, this file contains the uncompressed source as well as a bookmarklet version. Copy and paste the latter.

- Open another browser tab to any website and click the bookmarklet. You should start seeing the page stream from this new tab to the tab in #3. Scrolling the site should
be reflected on the screenshot viewer.

# Issues?

- Make sure `HOST = 'localhost:8000'` in `bookmarklet.js` points to your webserver's host.
- Make sure `WS_HOST = 'localhost:3000'` in `app.js` points to the correct location.
32 changes: 32 additions & 0 deletions screenshoter/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
window.URL = window.URL || window.webkitURL;

var WS_HOST = 'localhost:3000';
var ws = null;
var REFRESH_EVERY = 1000; // ms

function connect() {
ws = new WebSocket('ws://' + WS_HOST, ['dumby-protocol']);
ws.binaryType = 'blob';

ws.onopen = function(e) {
console.log('WebSocket connection OPEN');
};

ws.onclose = function(e) {
console.log('WebSocket connection CLOSED');
};

ws.onerror = function(e) {
console.log('WebSocket connection ERROR', e);
};

return ws;
}

function send() {
setInterval(function() {
if (ws.bufferedAmount == 0) {
ws.send(screenshotPage());
}
}, REFRESH_EVERY);
}
25 changes: 25 additions & 0 deletions screenshoter/bookmarklet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
(function() {
var HOST = 'localhost:8000';

var s = document.createElement('script');
s.type = 'text/javascript';
s.defer = true;
s.src = 'http://' + HOST + '/screencapture.js'

var s2 = document.createElement('script');
s2.type = 'text/javascript';
s2.defer = true;
s2.src = 'http://' + HOST + '/app.js'
s2.onload = function(e) {
connect();
send();
};

var scripts = document.getElementsByTagName('script')[0];
scripts.parentNode.insertBefore(s, scripts);
scripts.parentNode.insertBefore(s2, scripts);
})();

/* Bookmarklet version. Create a bookmark and save this as its URL:
javascript:(function(){var b=document.createElement("script");b.type="text/javascript";b.defer=!0;b.src="http://localhost:8000/screencapture.js";var a=document.createElement("script");a.type="text/javascript";a.defer=!0;a.src="http://localhost:8000/app.js";a.onload=function(){connect();send()};var c=document.getElementsByTagName("script")[0];c.parentNode.insertBefore(b,c);c.parentNode.insertBefore(a,c)})();
*/
114 changes: 114 additions & 0 deletions screenshoter/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<!DOCTYPE html>
<!--
Copyright 2012 Eric Bidelman
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Author: Eric Bidelman (ebidel@gmail.com)
-->
<html>
<head>
<title>Binary WebSocket Tab Sharing</title>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="chrome=1">
<link rel="stylesheet" href="http://fonts.googleapis.com/css?family=Orbitron|Kameron|Muli|Open+Sans:regular,semibold,italic,italicsemibold|Droid+Sans+Mono&v2">
<style>
body {
font-family: 'Open Sans';
}
iframe, #screenshot0, #screenshot1 {
width: 100%;
height: 500px;
border: 1px solid black;
}
</style>
</head>
<body>

<h3>Screenshot from host:</h3>
<iframe id="screenshot0" hidden></iframe>
<iframe id="screenshot1" hidden></iframe>

<div id="frames-container"></div>

<script src="screencapture.js"></script>
<script src="app.js"></script>
<script>
var framesContainer = document.querySelector('#frames-container');
var currentFrameIdx = 0;
var playbackIntervalId = null;
var ws = connect();

ws.onmessage = function(e) {
// Shouldn't have to create a new blob, but e.data isn't preserving type.
var blob = new Blob([e.data], {type: 'text/html'});

var iframe = document.createElement('iframe');
iframe.src = window.URL.createObjectURL(blob);
iframe.hidden = true;
iframe.onload = renderFrame;

// Force the iframe content to load by appending to the DOM.
framesContainer.appendChild(iframe);
};

function renderFrame() {
if (framesContainer.children.length) {
var frame = framesContainer.children[currentFrameIdx];

if(!frame) {
return;
}

if (currentFrameIdx > 0) {
var prevFrame = frame.previousElementSibling;
prevFrame.hidden = true;
window.URL.revokeObjectURL(prevFrame.src);
}

frame.hidden = false;

currentFrameIdx++;
}
}

function playback(interval) {
clearInterval(playbackIntervalId);

if (!framesContainer.children.length) {
return;
}

var i = 0;
playbackIntervalId = setInterval(function() {
var iframe = framesContainer.children[i];
if (i > 0) {
framesContainer.children[i - 1].hidden = true;
} else if (i == 0) {
framesContainer.children[framesContainer.children.length - 1].hidden = true;
}
iframe.hidden = false;

i++;
i %= framesContainer.children.length;
}, interval);
}

//setInterval(renderFrame, 1000);
</script>
<!--[if IE]>
<script src="http://ajax.googleapis.com/ajax/libs/chrome-frame/1/CFInstall.min.js"></script>
<script>CFInstall.check({mode: 'overlay'});</script>
<![endif]-->
</body>
</html>
103 changes: 103 additions & 0 deletions screenshoter/screencapture.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Copyright 2012 Eric Bidelman
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Author: Eric Bidelman (ebidel@)

(function(exports) {
function urlsToAbsolute(nodeList) {
if (!nodeList.length) {
return [];
}

var attrName = 'href';
if (nodeList[0].__proto__ === HTMLImageElement.prototype ||
nodeList[0].__proto__ === HTMLScriptElement.prototype) {
attrName = 'src';
}

nodeList = [].map.call(nodeList, function(el, i) {
var attr = el.getAttribute(attrName);
// If no src/href is present, disregard.
if (!attr) {
return;
}

var absURL = /^(https?|data):/i.test(attr);
if (absURL) {
return el;
} else {
if (attr.indexOf('/') != 0) { // src="images/test.jpg"
el.setAttribute(attrName, document.location.origin + document.location.pathname + attr);
} else if (attr.match(/^\/\//)) { // src="//static.server/test.jpg"
el.setAttribute(attrName, document.location.protocol + attr);
} else {
el.setAttribute(attrName, document.location.origin + attr);
}
return el;
}
});
return nodeList;
}

// TODO: current limitation is css background images are not included.
function screenshotPage() {
// 1. Rewrite current doc's imgs, css, and script URLs to be absolute before
// we duplicate. This ensures no broken links when viewing the duplicate.
urlsToAbsolute(document.images);
urlsToAbsolute(document.querySelectorAll("link[rel='stylesheet']"));
//urlsToAbsolute(document.scripts);

// 2. Duplicate entire document.
var screenshot = document.documentElement.cloneNode(true);

// 3. Screenshot should be readyonly, no scrolling, and no selections.
screenshot.style.pointerEvents = 'none';
screenshot.style.overflow = 'hidden';
screenshot.style.webkitUserSelect = 'none';
screenshot.style.mozUserSelect = 'none';
screenshot.style.msUserSelect = 'none';
screenshot.style.oUserSelect = 'none';
screenshot.style.userSelect = 'none';

// 4. Preserve current x,y scroll position of this page. See addOnPageLoad_().
screenshot.dataset.scrollX = window.scrollX;
screenshot.dataset.scrollY = window.scrollY;

// 4.5. When the screenshot loads (e.g. as ablob URL, as iframe.src, etc.),
// scroll it to the same location of this page. Do this by appending a
// window.onDOMContentLoaded listener which pulls out the saved scrollX/Y
// state from the DOM.
var script = document.createElement('script');
script.textContent = '(' + addOnPageLoad_.toString() + ')();'; // self calling.
screenshot.querySelector('body').appendChild(script);

// 5. Create a new .html file from the cloned content.
var blob = new Blob([screenshot.outerHTML], {type: 'text/html'});

return blob;
}

// NOTE: Not to be invoked directly. When the screenshot loads, it should scroll
// to the same x,y location of this page.
function addOnPageLoad_() {
window.addEventListener('DOMContentLoaded', function(e) {
var scrollX = document.documentElement.dataset.scrollX || 0;
var scrollY = document.documentElement.dataset.scrollY || 0;
window.scrollTo(scrollX, scrollY);
});
}

exports.screenshotPage = screenshotPage;

})(window);
60 changes: 60 additions & 0 deletions screenshoter/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/usr/bin/env node

var WebSocketServer = require('ws').Server;

var http = require('http');
var url = require('url');
var fs = require('fs');

var args = { /* defaults */
port: '8080'
};

/* Parse command line options */
var pattern = /^--(.*?)(?:=(.*))?$/;
process.argv.forEach(function(value) {
var match = pattern.exec(value);
if (match) {
args[match[1]] = match[2] ? match[2] : true;
}
});

var port = parseInt(args.port, 10);

console.log("Usage: ./server.js [--port=8080]");

var connections = {};

// ws is the fastest websocket lib - http://einaros.github.com/ws/
var wsServer = new WebSocketServer({port: port});

wsServer.on('connection', function(ws) {

ws.id = Date.now(); // Assign unique id to this ws connection.
connections[ws.id] = ws;

console.log((new Date()) + ' Connection accepted: ' + ws.id);

ws.on('message', function(message, flags) {
console.log('Received ' + (flags.binary ? 'binary' : '') + ' message: ' +
message.length + ' bytes.');
broadcast(message, this, flags);
});

ws.on('close', function() {
console.log((new Date()) + " Peer " + this.id + " disconnected.");
delete connections[this.id];
});
});

// Broadcasts a message to all connected sockets accept for the sender.
function broadcast(message, fromWs, flags) {
for (var id in connections) {
if (id != fromWs.id) {
connections[id].send(message, {
binary: flags.binary ? true : false,
mask: false
});
}
}
}

0 comments on commit 5d54fc7

Please sign in to comment.