├── README.md
├── background.html
├── cws
└── screenshot.png
├── images
├── icon-rec.png
├── icon.png
├── pause.png
├── play.png
├── still.png
└── video.png
├── manifest.json
├── oauth2
├── README.md
├── adapters
│ ├── facebook.js
│ ├── github.js
│ ├── google.js
│ └── sample.js
├── oauth2.html
├── oauth2.js
└── oauth2_inject.js
├── picasa.js
├── playback.css
├── playback.html
├── playback.js
├── screencast.md
└── screenshot.js
/README.md:
--------------------------------------------------------------------------------
1 | # Video screencast sample
2 |
3 | Make video screencasts of a chrome tab, and play them back right in the
4 | browser. Share stills to Picasa.
5 |
--------------------------------------------------------------------------------
/background.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Web Flow Background
4 |
5 |
6 |
7 |
8 |
9 |
10 |
12 |
13 |
15 |
16 |
--------------------------------------------------------------------------------
/cws/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/borismus/chrome-screencast/466b0faab989c65dc2121ed41e2df15781eea8b5/cws/screenshot.png
--------------------------------------------------------------------------------
/images/icon-rec.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/borismus/chrome-screencast/466b0faab989c65dc2121ed41e2df15781eea8b5/images/icon-rec.png
--------------------------------------------------------------------------------
/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/borismus/chrome-screencast/466b0faab989c65dc2121ed41e2df15781eea8b5/images/icon.png
--------------------------------------------------------------------------------
/images/pause.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/borismus/chrome-screencast/466b0faab989c65dc2121ed41e2df15781eea8b5/images/pause.png
--------------------------------------------------------------------------------
/images/play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/borismus/chrome-screencast/466b0faab989c65dc2121ed41e2df15781eea8b5/images/play.png
--------------------------------------------------------------------------------
/images/still.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/borismus/chrome-screencast/466b0faab989c65dc2121ed41e2df15781eea8b5/images/still.png
--------------------------------------------------------------------------------
/images/video.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/borismus/chrome-screencast/466b0faab989c65dc2121ed41e2df15781eea8b5/images/video.png
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Screencast",
3 | "version": "0.3",
4 | "description": "Records a video of your chrome browser. Lets you share out the video or stills of the video.",
5 | "icons": {
6 | "128": "images/icon.png",
7 | "48": "images/icon.png",
8 | "16": "images/icon.png"
9 | },
10 | "background_page": "background.html",
11 | "browser_action": {
12 | "default_title": "Start recording.",
13 | "default_icon": "images/icon.png"
14 | },
15 | "content_scripts": [
16 | {
17 | "matches": ["http://www.google.com/robots.txt*"],
18 | "js": ["oauth2/oauth2_inject.js"]
19 | }
20 | ],
21 | "permissions": ["tabs", ""]
22 | }
23 |
--------------------------------------------------------------------------------
/oauth2/README.md:
--------------------------------------------------------------------------------
1 | OAuth 2.0 Library for Chrome Extensions
2 |
3 | # Goals
4 |
5 | 1. Avoid background pages
6 | 2. Support a variety of OAuth 2.0 providers that implement the spec
7 | 3. Allow multiple applications to use different OAuth 2.0 endpoints from
8 | one chrome extension.
9 |
10 | TODO:
11 | * Proper error handling
12 | * Unit tests based on Google's implementation at least
13 |
14 | # Flow
15 |
16 | 1. Open popup to get an authorizationCode. Specify an implementation
17 | specific redirect (ex. google.com/robots.txt)
18 | 2. User fills out a form (or is already logged in) and then allows
19 | access to the Allow button
20 | 3. OAuth 2.0 server calls back with REDIRECT?code=THE_CODE which is
21 | intercepted by an injected script which then calls back to the
22 | extension itself. Also provided (along with code and/or errors) is
23 | the invoking URL.
24 | 4. Now in oauth2.html, look up which adapter we called (by doing a
25 | lookup based on the invoking URL)
26 |
27 | # User setup
28 |
29 | * Add oauth2_inject.js to http://www.oauth.net/robots.txt injected
30 | scripts in the manifest:
31 |
32 | "content_scripts": [
33 | {
34 | "matches": ["http://www.oauth.net/robots.txt*"],
35 | "js": ["oauth2_inject.js"]
36 | }
37 | ],
38 |
39 | * Initialize the OAuth 2.0 sessions you care about:
40 |
41 | var myAuth = new OAuth2(adapterName, {
42 | clientId: foo,
43 | clientSecret: bar,
44 | apiScope: baz,
45 | });
46 |
47 | which may open an Allow dialog. Can also do
48 |
49 | var myAuth = new OAuth2(adapterName);
50 |
51 | if relying on previously saved credentials.
52 |
53 | * Authorize the sessions
54 |
55 | myAuth.authorize(function() {
56 | // Successfully authorized
57 | });
58 |
59 | * Get token via accessToken = myAuth.getAccessToken()
60 |
61 | # Implementation-specific information
62 |
63 | * Authorization Code URL format (authorizationCodeURLFormat)
64 |
65 | https://accounts.google.com/o/oauth2/auth?
66 | client_id={{CLIENT_ID}&
67 | redirect_uri={{REDIRECT_URI}}&
68 | scope={{SCOPE}}&
69 | response_type=code
70 |
71 | or
72 |
73 | https://www.facebook.com/dialog/oauth?
74 | client_id={{CLIENT_ID}}&redirect_uri={{REDIRECT_URI -- suffixed with ?type=code}}
75 |
76 | * Redirect URL prefix (redirectURLPrefix)
77 |
78 | http://www.oauth.net/robots.txt
79 |
80 | * A function to parse the response given by the URL and handle errors
81 | (parseAuthorizationCode)
82 |
83 | https://www.example.com/back?code=4/P7q7W91a-oMsCeLvIaQm6bTrgtp6
84 |
85 | or
86 |
87 | http://www.example.com/foo?code=73iuhfHx98FKJr
88 |
89 |
90 | * URL and params for fetching access tokens (accessTokenURL,
91 | accessTokenMethod, accessTokenParams)
92 |
93 | POST https://accounts.google.com/o/oauth2/token
94 |
95 | code={{AUTHORIZATION_CODE}}
96 | client_id={{CLIENT_ID}}
97 | client_secret={{CLIENT_SECRET}}
98 | redirect_uri={{REDIRECT_URI -- suffixed with ?type=token}}
99 | grant_type=authorization_code
100 |
101 |
102 | or
103 |
104 | https://graph.facebook.com/oauth/access_token?
105 | client_id={{CLIENT_ID}}&redirect_uri={{REDIRECT_URI}}&
106 | client_secret={{CLIENT_SECRET}}&code={{AUTHORIZATION_CODE}}
107 |
108 |
109 | * A function to parse the response given by the access token endpoint
110 | (parseAccessToken)
111 |
112 | {
113 | "access_token":"1/fFAGRNJru1FTz70BzhT3Zg",
114 | "expires_in":3920,
115 | "refresh_token":"1/6BMfW9j53gdGImsixUH6kU5RsR4zwI9lUVX-tqf8JXQ"
116 | }
117 |
118 | or
119 |
120 | access_token=135198374987134|JKHASf7868v&expires=5108
121 |
--------------------------------------------------------------------------------
/oauth2/adapters/facebook.js:
--------------------------------------------------------------------------------
1 | OAuth2.adapter('facebook', {
2 | authorizationCodeURL: function(config) {
3 | return 'https://www.facebook.com/dialog/oauth? \
4 | client_id={{CLIENT_ID}}&redirect_uri={{REDIRECT_URI}}&scope={{API_SCOPE}}'
5 | .replace('{{CLIENT_ID}}', config.clientId)
6 | .replace('{{REDIRECT_URI}}', this.redirectURL(config))
7 | .replace('{{API_SCOPE}}', config.apiScope);
8 | },
9 |
10 | redirectURL: function(config) {
11 | return 'http://www.facebook.com/robots.txt';
12 | },
13 |
14 | parseAuthorizationCode: function(url) {
15 | // TODO: error handling (URL may have
16 | // ?error=asfasfasiof&error_code=43 etc)
17 | return url.match(/\?code=(.+)/)[1];
18 | },
19 |
20 | accessTokenURL: function() {
21 | return 'https://graph.facebook.com/oauth/access_token';
22 | },
23 |
24 | accessTokenMethod: function() {
25 | return 'GET';
26 | },
27 |
28 | accessTokenParams: function(authorizationCode, config) {
29 | return {
30 | code: authorizationCode,
31 | client_id: config.clientId,
32 | client_secret: config.clientSecret,
33 | redirect_uri: this.redirectURL(config)
34 | }
35 | },
36 |
37 | parseAccessToken: function(response) {
38 | return {
39 | accessToken: response.match(/access_token=([^&]*)/)[1],
40 | expiresIn: response.match(/expires=([^&]*)/)[1]
41 | }
42 | }
43 | });
44 |
--------------------------------------------------------------------------------
/oauth2/adapters/github.js:
--------------------------------------------------------------------------------
1 | OAuth2.adapter('github', {
2 | /**
3 | * @return {URL} URL to the page that returns the authorization code
4 | */
5 | authorizationCodeURL: function(config) {
6 | return 'https://github.com/login/oauth/authorize?\
7 | client_id={{CLIENT_ID}}&redirect_uri={{REDIRECT_URI}}'
8 | .replace('{{CLIENT_ID}}', config.clientId)
9 | .replace('{{REDIRECT_URI}}', this.redirectURL(config));
10 | },
11 |
12 | /**
13 | * @return {URL} URL to the page that we use to inject the content
14 | * script into
15 | */
16 | redirectURL: function(config) {
17 | return 'https://github.com/robots.txt';
18 | },
19 |
20 | /**
21 | * @return {String} Authorization code for fetching the access token
22 | */
23 | parseAuthorizationCode: function(url) {
24 | var error = url.match(/\?error=(.+)/);
25 | if (error) {
26 | throw 'Error getting authorization code: ' + error[1];
27 | }
28 | return url.match(/\?code=([\w\/\-]+)/)[1];
29 | },
30 |
31 | /**
32 | * @return {URL} URL to the access token providing endpoint
33 | */
34 | accessTokenURL: function() {
35 | return 'https://github.com/login/oauth/access_token';
36 | },
37 |
38 | /**
39 | * @return {String} HTTP method to use to get access tokens
40 | */
41 | accessTokenMethod: function() {
42 | return 'POST';
43 | },
44 |
45 | /**
46 | * @return {Object} The payload to use when getting the access token
47 | */
48 | accessTokenParams: function(authorizationCode, config) {
49 | return {
50 | code: authorizationCode,
51 | client_id: config.clientId,
52 | client_secret: config.clientSecret,
53 | redirect_uri: this.redirectURL(config),
54 | grant_type: 'authorization_code'
55 | }
56 | },
57 |
58 | /**
59 | * @return {Object} Object containing accessToken {String},
60 | * refreshToken {String} and expiresIn {Int}
61 | */
62 | parseAccessToken: function(response) {
63 | return {
64 | accessToken: response.match(/access_token=([^&]*)/)[1],
65 | expiresIn: Number.MAX_VALUE
66 | }
67 | }
68 | });
69 |
--------------------------------------------------------------------------------
/oauth2/adapters/google.js:
--------------------------------------------------------------------------------
1 | OAuth2.adapter('google', {
2 | authorizationCodeURL: function(config) {
3 | return 'https://accounts.google.com/o/oauth2/auth?\
4 | client_id={{CLIENT_ID}}&\
5 | redirect_uri={{REDIRECT_URI}}&\
6 | scope={{API_SCOPE}}&\
7 | response_type=code'
8 | .replace('{{CLIENT_ID}}', config.clientId)
9 | .replace('{{REDIRECT_URI}}', this.redirectURL(config))
10 | .replace('{{API_SCOPE}}', config.apiScope);
11 | },
12 |
13 | redirectURL: function(config) {
14 | return 'http://www.google.com/robots.txt';
15 | },
16 |
17 | parseAuthorizationCode: function(url) {
18 | var error = url.match(/\?error=(.+)/);
19 | if (error) {
20 | throw 'Error getting authorization code: ' + error[1];
21 | }
22 | return url.match(/\?code=([\w\/\-]+)/)[1];
23 | },
24 |
25 | accessTokenURL: function() {
26 | return 'https://accounts.google.com/o/oauth2/token';
27 | },
28 |
29 | accessTokenMethod: function() {
30 | return 'POST';
31 | },
32 |
33 | accessTokenParams: function(authorizationCode, config) {
34 | return {
35 | code: authorizationCode,
36 | client_id: config.clientId,
37 | client_secret: config.clientSecret,
38 | redirect_uri: this.redirectURL(config),
39 | grant_type: 'authorization_code'
40 | }
41 | },
42 |
43 | parseAccessToken: function(response) {
44 | var parsedResponse = JSON.parse(response);
45 | return {
46 | accessToken: parsedResponse.access_token,
47 | refreshToken: parsedResponse.refresh_token,
48 | expiresIn: parsedResponse.expires_in
49 | }
50 | }
51 | });
52 |
--------------------------------------------------------------------------------
/oauth2/adapters/sample.js:
--------------------------------------------------------------------------------
1 | OAuth2.adapter('sample', {
2 | /**
3 | * @return {URL} URL to the page that returns the authorization code
4 | */
5 | authorizationCodeURL: function(config) {
6 | return '';
7 | },
8 |
9 | /**
10 | * @return {URL} URL to the page that we use to inject the content
11 | * script into
12 | */
13 | redirectURL: function(config) {
14 | return '';
15 | },
16 |
17 | /**
18 | * @return {String} Authorization code for fetching the access token
19 | */
20 | parseAuthorizationCode: function(url) {
21 | return '';
22 | },
23 |
24 | /**
25 | * @return {URL} URL to the access token providing endpoint
26 | */
27 | accessTokenURL: function() {
28 | return 'https://accounts.google.com/o/oauth2/token';
29 | },
30 |
31 | /**
32 | * @return {String} HTTP method to use to get access tokens
33 | */
34 | accessTokenMethod: function() {
35 | return 'POST';
36 | },
37 |
38 | /**
39 | * @return {Object} The payload to use when getting the access token
40 | */
41 | accessTokenParams: function(authorizationCode, config) {
42 | return {};
43 | },
44 |
45 | /**
46 | * @return {Object} Object containing accessToken {String},
47 | * refreshToken {String} and expiresIn {Int}
48 | */
49 | parseAccessToken: function(response) {
50 | return {
51 | accessToken: '',
52 | refreshToken: '',
53 | expiresIn: 0
54 | }
55 | }
56 | });
57 |
--------------------------------------------------------------------------------
/oauth2/oauth2.html:
--------------------------------------------------------------------------------
1 |
2 | OAuth 2.0 Finish Page
3 |
5 |
6 |
7 |
8 |
13 |
--------------------------------------------------------------------------------
/oauth2/oauth2.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Constructor
3 | *
4 | * @param {String} adapterName name of the adapter to use for this OAuth 2
5 | * @param {Object} config Containing clientId, clientSecret and apiScope
6 | * @param {String} config Alternatively, OAuth2.FINISH for the finish flow
7 | */
8 | var OAuth2 = function(adapterName, config, callback) {
9 | this.adapterName = adapterName;
10 | var that = this;
11 | OAuth2.loadAdapter(adapterName, function() {
12 | that.adapter = OAuth2.adapters[adapterName];
13 | if (config == OAuth2.FINISH) {
14 | that.finishAuth();
15 | return;
16 | } else if (config) {
17 | that.set('clientId', config.client_id);
18 | that.set('clientSecret', config.client_secret);
19 | that.set('apiScope', config.api_scope);
20 | }
21 | });
22 | };
23 |
24 | /**
25 | *
26 | */
27 | OAuth2.FINISH = 'finish';
28 |
29 | /**
30 | * OAuth 2.0 endpoint adapters known to the library
31 | */
32 | OAuth2.adapters = {};
33 | OAuth2.adapterReverse = localStorage.adapterReverse &&
34 | JSON.parse(localStorage.adapterReverse) || {};
35 |
36 | /**
37 | * Opens up an authorization popup window. This starts the OAuth 2.0 flow.
38 | */
39 | OAuth2.prototype.openAuthorizationCodePopup = function() {
40 | // Create a new tab with the OAuth 2.0 prompt
41 | chrome.tabs.create({url: this.adapter.authorizationCodeURL(this.getConfig())},
42 | function(tab) {
43 | // 1. user grants permission for the application to access the OAuth 2.0
44 | // endpoint
45 | // 2. the endpoint redirects to the redirect URL.
46 | // 3. the extension injects a script into that redirect URL
47 | // 4. the injected script redirects back to oauth2.html, also passing
48 | // the redirect URL
49 | // 5. oauth2.html uses redirect URL to know what OAuth 2.0 flow to finish
50 | // (if there are multiple OAuth 2.0 adapters)
51 | // 6. Finally, the flow is finished and client code can call
52 | // myAuth.getAccessToken() to get a valid access token.
53 | });
54 | };
55 |
56 | /**
57 | * Gets access and refresh (if provided by endpoint) tokens
58 | *
59 | * @param {String} authorizationCode Retrieved from the first step in the process
60 | * @param {Function} callback Called back with 3 params:
61 | * access token, refresh token and expiry time
62 | */
63 | OAuth2.prototype.getAccessAndRefreshTokens = function(authorizationCode, callback) {
64 | var that = this;
65 | // Make an XHR to get the token
66 | var xhr = new XMLHttpRequest();
67 | xhr.onreadystatechange = function(event) {
68 | if (xhr.readyState == 4) {
69 | if(xhr.status == 200) {
70 | var obj = that.adapter.parseAccessToken(xhr.responseText);
71 | // Callback with the tokens
72 | callback(obj.accessToken, obj.refreshToken, obj.expiresIn);
73 | }
74 | }
75 | };
76 |
77 | var method = that.adapter.accessTokenMethod();
78 | var items = that.adapter.accessTokenParams(authorizationCode, that.getConfig());
79 | var key = null;
80 | if (method == 'POST') {
81 | var formData = new FormData();
82 | for (key in items) {
83 | formData.append(key, items[key]);
84 | }
85 | xhr.open(method, that.adapter.accessTokenURL(), true);
86 | xhr.send(formData);
87 | } else if (method == 'GET') {
88 | var url = that.adapter.accessTokenURL();
89 | var params = '?';
90 | for (key in items) {
91 | params += key + '=' + items[key] + '&';
92 | }
93 | xhr.open(method, url + params, true);
94 | xhr.send();
95 | } else {
96 | throw method + ' is an unknown method';
97 | }
98 | };
99 |
100 | /**
101 | * Refreshes the access token using the currently stored refresh token
102 | * Note: this only happens for the Google adapter since all other OAuth 2.0
103 | * endpoints don't implement refresh tokens.
104 | *
105 | * @param {String} refreshToken A valid refresh token
106 | * @param {Function} callback On success, called with access token and expiry time
107 | */
108 | OAuth2.prototype.refreshAccessToken = function(refreshToken, callback) {
109 | var xhr = new XMLHttpRequest();
110 | xhr.onreadystatechange = function(event) {
111 | if (xhr.readyState == 4) {
112 | if(xhr.status == 200) {
113 | console.log(xhr.responseText);
114 | // Parse response with JSON
115 | obj = JSON.parse(xhr.responseText);
116 | // Callback with the tokens
117 | callback(obj.access_token, obj.expires_in);
118 | }
119 | }
120 | };
121 |
122 | var formData = new FormData();
123 | formData.append('client_id', this.get('clientId'));
124 | formData.append('client_secret', this.get('clientSecret'));
125 | formData.append('refresh_token', this.get('refreshToken'));
126 | formData.append('grant_type', 'refresh_token');
127 | xhr.open('POST', this.adapter.accessTokenURL(), true);
128 | xhr.send(formData);
129 | };
130 |
131 | /**
132 | * Extracts authorizationCode from the URL and makes a request to the last
133 | * leg of the OAuth 2.0 process.
134 | */
135 | OAuth2.prototype.finishAuth = function() {
136 | var that = this;
137 | var authorizationCode = that.adapter.parseAuthorizationCode(window.location.href);
138 | console.log(authorizationCode);
139 | that.getAccessAndRefreshTokens(authorizationCode, function(at, rt, exp) {
140 | that.set('accessToken', at);
141 | that.set('expiresIn', exp);
142 | // Most OAuth 2.0 providers don't have a refresh token
143 | if (rt) {
144 | that.set('refreshToken', rt);
145 | }
146 | that.set('accessTokenDate', (new Date()).valueOf());
147 |
148 | // Once we get here, close the current tab and we're good to go.
149 | window.open('', '_self', ''); //bug fix
150 | window.close();
151 | });
152 | };
153 |
154 | /**
155 | * @return True iff the current access token has expired
156 | */
157 | OAuth2.prototype.isAccessTokenExpired = function() {
158 | return (new Date().valueOf() - this.get('accessTokenDate')) >
159 | this.get('expiresIn') * 1000;
160 | };
161 |
162 | /**
163 | * Wrapper around the localStorage object that gets variables prefixed
164 | * by the adapter name
165 | *
166 | * @param {String} key The key to use for lookup
167 | * @return {String} The value
168 | */
169 | OAuth2.prototype.get = function(key) {
170 | return localStorage[this.adapterName + '_' + key];
171 | };
172 |
173 | /**
174 | * Wrapper around the localStorage object that sets variables prefixed
175 | * by the adapter name
176 | *
177 | * @param {String} key The key to store with
178 | * @param {String} value The value to store
179 | */
180 | OAuth2.prototype.set = function(key, value) {
181 | localStorage[this.adapterName + '_' + key] = value;
182 | };
183 |
184 | /**
185 | * The configuration parameters that are passed to the adapter
186 | *
187 | * @returns {Object} Containing clientId, clientSecret and apiScope
188 | */
189 | OAuth2.prototype.getConfig = function() {
190 | return {
191 | clientId: this.get('clientId'),
192 | clientSecret: this.get('clientSecret'),
193 | apiScope: this.get('apiScope')
194 | }
195 | };
196 |
197 | /***********************************
198 | *
199 | * STATIC ADAPTER RELATED METHODS
200 | *
201 | ***********************************/
202 |
203 | /**
204 | * Loads an OAuth 2.0 adapter and calls back when it's loaded
205 | *
206 | * @param adapterName {String} The name of the JS file
207 | * @param callback {Function} Called as soon as the adapter has been loaded
208 | */
209 | OAuth2.loadAdapter = function(adapterName, callback) {
210 | // If it's already loaded, don't load it again
211 | if (OAuth2.adapters[adapterName]) {
212 | callback();
213 | return;
214 | }
215 | var head = document.querySelector('head');
216 | var script = document.createElement('script');
217 | script.type = 'text/javascript';
218 | script.src = '/oauth2/adapters/' + adapterName + '.js';
219 | script.addEventListener('load', function() {
220 | callback();
221 | });
222 | head.appendChild(script);
223 | };
224 |
225 | /**
226 | * Registers an adapter with the library. This call is used by each adapter
227 | *
228 | * @param {String} name The adapter name
229 | * @param {Object} impl The adapter implementation
230 | *
231 | * @throws {String} If the specified adapter is invalid
232 | */
233 | OAuth2.adapter = function(name, impl) {
234 | var implementing = 'authorizationCodeURL redirectURL \
235 | accessTokenURL accessTokenMethod accessTokenParams accessToken';
236 |
237 | // Check for missing methods
238 | implementing.split(' ').forEach(function(method, index) {
239 | if (!method in impl) {
240 | throw 'Invalid adapter! Missing method: ' + method;
241 | }
242 | });
243 |
244 | // Save the adapter in the adapter registry
245 | OAuth2.adapters[name] = impl;
246 | // Make an entry in the adapter lookup table
247 | OAuth2.adapterReverse[impl.redirectURL()] = name;
248 | // Store the the adapter lookup table in localStorage
249 | localStorage.adapterReverse = JSON.stringify(OAuth2.adapterReverse);
250 | };
251 |
252 | /**
253 | * Looks up the adapter name based on the redirect URL. Used by oauth2.html
254 | * in the second part of the OAuth 2.0 flow.
255 | *
256 | * @param {String} url The url that called oauth2.html
257 | * @return The adapter for the current page
258 | */
259 | OAuth2.lookupAdapterName = function(url) {
260 | var adapterReverse = JSON.parse(localStorage.adapterReverse);
261 | return adapterReverse[url];
262 | };
263 |
264 | /***********************************
265 | *
266 | * PUBLIC API
267 | *
268 | ***********************************/
269 |
270 | /**
271 | * Authorizes the OAuth authenticator instance.
272 | *
273 | * @param {Function} callback Tries to callback when auth is successful
274 | * Note: does not callback if grant popup required
275 | */
276 | OAuth2.prototype.authorize = function(callback) {
277 | var that = this;
278 | OAuth2.loadAdapter(that.adapterName, function() {
279 | that.adapter = OAuth2.adapters[that.adapterName];
280 | if (!that.get('accessToken')) {
281 | // There's no access token yet. Start the authorizationCode flow
282 | that.openAuthorizationCodePopup();
283 | } else if (that.isAccessTokenExpired()) {
284 | // There's an existing access token but it's expired
285 | if (that.get('refreshToken')) {
286 | that.refreshAccessToken(that.get('refreshToken'), function(at, exp) {
287 | that.set('accessToken', at);
288 | that.set('expiresIn', exp);
289 | that.set('accessTokenDate', (new Date()).valueOf());
290 | // Callback when we finish refreshing
291 | if (callback) {
292 | callback();
293 | }
294 | });
295 | } else {
296 | // No refresh token... just do the popup thing again
297 | that.openAuthorizationCodePopup();
298 | }
299 | } else {
300 | // We have an access token, and it's not expired yet
301 | if (callback) {
302 | callback();
303 | }
304 | }
305 | });
306 | }
307 |
308 | /**
309 | * @returns A valid access token.
310 | */
311 | OAuth2.prototype.getAccessToken = function() {
312 | return this.get('accessToken');
313 | };
314 |
--------------------------------------------------------------------------------
/oauth2/oauth2_inject.js:
--------------------------------------------------------------------------------
1 | // This script servers as an intermediary between oauth2.js and
2 | // oauth2.html
3 |
4 | // Get all ? params from this URL
5 | var url = window.location.href;
6 | var params = url.substring(url.indexOf('?'));
7 |
8 | // Also append the current URL to the params
9 | params += '&from=' + escape(url);
10 |
11 | // Redirect back to the extension itself so that we have priveledged
12 | // access again
13 | var redirect = chrome.extension.getURL('oauth2/oauth2.html');
14 | window.location = redirect + params;
15 |
--------------------------------------------------------------------------------
/picasa.js:
--------------------------------------------------------------------------------
1 | var google = new OAuth2('google', {
2 | client_id: '952993494713-h12m6utvq8g8d8et8n2i68plbrr6cr4d.apps.googleusercontent.com',
3 | client_secret: 'IZ4hBSbosuhoWAX4lyAomm-R',
4 | api_scope: 'https://www.googleapis.com/auth/photos'
5 | });
6 |
7 | var ALBUM_NAME = 'screenshots';
8 | var CREATE_ALBUM_URL = 'https://picasaweb.google.com/data/feed/api/user/default?alt=json';
9 | var LIST_ALBUM_URL = 'https://picasaweb.google.com/data/feed/api/user/default?alt=json';
10 | var CREATE_PHOTO_URL = 'https://picasaweb.google.com/data/feed/api/user/default/albumid/{{albumId}}?alt=json';
11 | var SHORTENER_URL = 'https://www.googleapis.com/urlshortener/v1/url';
12 |
13 | function createAlbum(albumName, callback) {
14 | google.authorize(function() {
15 | var xhr = new XMLHttpRequest();
16 | xhr.open('POST', CREATE_ALBUM_URL, true);
17 | xhr.setRequestHeader('Authorization', 'OAuth ' + google.getAccessToken());
18 | xhr.setRequestHeader('Content-Type', 'application/json');
19 | xhr.onload = function(e) {
20 | if (this.status == 200) {
21 | var data = JSON.parse(xhr.responseText);
22 | console.log('created album', data);
23 | if (callback) {
24 | callback(data);
25 | }
26 | }
27 | };
28 | xhr.send(JSON.stringify({
29 | displayName: albumName
30 | }));
31 | });
32 | }
33 |
34 | function getAlbum(albumName, callback) {
35 | google.authorize(function() {
36 | // List all albums and check if any are called albumName
37 | var xhr = new XMLHttpRequest();
38 | xhr.open('GET', LIST_ALBUM_URL, true);
39 | xhr.setRequestHeader('Authorization', 'OAuth ' + google.getAccessToken());
40 | xhr.onload = function(e) {
41 | if (this.status == 200) {
42 | var data = JSON.parse(xhr.responseText);
43 | var entries = data.feed.entry;
44 | //console.log('fetched albums', data);
45 | for (var i = 0; i < entries.length; i++) {
46 | var item = entries[i];
47 | if (item.title.$t == albumName) {
48 | //console.log(albumName);
49 | callback(item);
50 | return;
51 | }
52 | }
53 | callback();
54 | }
55 | };
56 | xhr.send();
57 | });
58 | }
59 |
60 | function getOrCreateAlbum(albumName, callback) {
61 | var albumId = null;
62 | getAlbum(albumName, function(album) {
63 | if (album) {
64 | albumId = album.gphoto$id.$t;
65 | callback(albumId);
66 | } else {
67 | createAlbum(albumName, function(album) {
68 | albumId = album.id;
69 | callback(albumId);
70 | });
71 | }
72 | });
73 | }
74 |
75 | function uploadPhoto(albumId, dataURI, callback, progressCallback) {
76 | google.authorize(function() {
77 | var url = CREATE_PHOTO_URL.replace('{{albumId}}', albumId);
78 | var xhr = new XMLHttpRequest();
79 | xhr.open('POST', url, true);
80 | xhr.setRequestHeader('Authorization', 'OAuth ' + google.getAccessToken());
81 | xhr.setRequestHeader('Content-Type', 'image/jpeg');
82 | xhr.onload = function(e) {
83 | if (this.status == 201) {
84 | var data = JSON.parse(xhr.responseText);
85 | // Get the URL to the raw image
86 | var url = getRawUrl(data);
87 | // Shorten the URL
88 | shortenURL(url, function(shortenedUrl) {
89 | callback({url: shortenedUrl});
90 | });
91 | }
92 | };
93 | if (progressCallback) {
94 | xhr.upload.onprogress = function(e) {
95 | console.log('progress', e.lengthComputable, e.loaded, e.total);
96 | if (e.lengthComputable) {
97 | progressCallback(e.loaded, e.total);
98 | }
99 | };
100 | }
101 | var array = convertDataURIToBinary(dataURI);
102 | xhr.send(array.buffer);
103 | });
104 | }
105 |
106 | function shortenURL(url, callback) {
107 | var xhr = new XMLHttpRequest();
108 | xhr.open('POST', SHORTENER_URL, true);
109 | xhr.setRequestHeader('Content-Type', 'application/json');
110 | xhr.onload = function(e) {
111 | if (this.status == 200) {
112 | var response = JSON.parse(xhr.responseText);
113 | callback(response.id);
114 | }
115 | };
116 | xhr.send(JSON.stringify({longUrl: url}));
117 | }
118 |
119 |
120 | var BASE64_MARKER = ';base64,';
121 | function convertDataURIToBinary(dataURI) {
122 | var base64Index = dataURI.indexOf(BASE64_MARKER) + BASE64_MARKER.length;
123 | var base64 = dataURI.substring(base64Index);
124 | var raw = window.atob(base64);
125 | var rawLength = raw.length;
126 | var array = new Uint8Array(new ArrayBuffer(rawLength));
127 |
128 | for(i = 0; i < rawLength; i++) {
129 | array[i] = raw.charCodeAt(i);
130 | }
131 | return array;
132 | }
133 |
134 | function getRawUrl(data) {
135 | var links = data.entry.link;
136 | for (var i = 0; i < links.length; i++) {
137 | var link = links[i];
138 | if (link.type == 'image/jpeg') {
139 | return link.href;
140 | }
141 | }
142 | return null;
143 | }
144 |
--------------------------------------------------------------------------------
/playback.css:
--------------------------------------------------------------------------------
1 | body {
2 | background: #333;
3 | }
4 |
5 | #container {
6 | width: 980px;
7 | margin: 20px auto;
8 | position: relative;
9 | }
10 |
11 | #image {
12 | width: 100%;
13 | -webkit-box-shadow: 10px 10px 5px #000;
14 | }
15 |
16 | #controls_container {
17 | position: absolute;
18 | bottom: 40px;
19 | width: 100%;
20 | }
21 |
22 | #controls {
23 | background-image: -webkit-gradient(
24 | linear,
25 | left bottom,
26 | left top,
27 | color-stop(0.26, rgba(30,30,30,50)),
28 | color-stop(0.63, rgba(70,70,70,50))
29 | );
30 | width: 600px;
31 | margin: 0 auto;
32 | border-radius: 5px;
33 | padding-bottom: 20px;
34 | }
35 |
36 | #share {
37 | float: right;
38 | }
39 |
40 | input[type=range] {
41 | -webkit-appearance: none;
42 | background-color: black;
43 | width: 600px;
44 | margin: 0 auto;
45 | height: 10px;
46 | }
47 |
48 | input[type="range"]::-webkit-slider-thumb {
49 | -webkit-appearance: none;
50 | background-color: silver;
51 | opacity: 0.9;
52 | width: 7px;
53 | height: 20px;
54 | }
55 |
56 |
57 | button {
58 | width: 48px;
59 | height: 48px;
60 | border: 0;
61 | opacity: 1;
62 | background-repeat: no-repeat;
63 | background-size: 32px;
64 | background-position: 8px 8px;
65 | background-color: transparent;
66 | border-radius: 10px;
67 | margin: 5px;
68 | }
69 | button:active {
70 | opacity: 0.5;
71 | }
72 | button:hover {
73 | -webkit-box-shadow: inset 0 0 20px #666;
74 | }
75 |
76 | #playpause {
77 | float: left;
78 | width: 64px;
79 | height: 64px;
80 | background-size: 48px;
81 | margin-left: 263px;
82 | }
83 |
84 | #playpause.play { background-image: url(images/play.png); }
85 | #playpause.pause { background-image: url(images/pause.png); }
86 | #still { background-image: url(images/still.png); }
87 | #video { background-image: url(images/video.png); }
88 |
89 | progress { width: 100% }
90 |
91 | #bottom {
92 | color: #ccc;
93 | font-family: Verdana, sans-serif;
94 | font-weight: bold;
95 | font-size: 16px;
96 | }
97 | #bottom > * { clear: both; display: none; }
98 | #bottom .playback { display: block; }
99 |
100 | #bottom .shared { padding: 15px; padding-bottom: 0; }
101 | #bottom .shared .view { text-align: right; }
102 | #bottom .shared .copy { font-size: 12px; font-weight: normal; }
103 | #bottom .shared .message { float: left; }
104 |
105 | #bottom .upload .progress {
106 | background-color: hsl(100, 57%, 40%);
107 | background-image: -webkit-repeating-linear-gradient(transparent, transparent 50px, rgba(0,0,0,.4) 50px, rgba(0,0,0,.4) 53px, transparent 53px, transparent 63px, rgba(0,0,0,.4) 63px, rgba(0,0,0,.4) 66px, transparent 66px, transparent 116px, rgba(0,0,0,.5) 116px, rgba(0,0,0,.5) 166px, rgba(255,255,255,.2) 166px, rgba(255,255,255,.2) 169px, rgba(0,0,0,.5) 169px, rgba(0,0,0,.5) 179px, rgba(255,255,255,.2) 179px, rgba(255,255,255,.2) 182px, rgba(0,0,0,.5) 182px, rgba(0,0,0,.5) 232px, transparent 232px),
108 | -webkit-repeating-linear-gradient(-35deg, transparent, transparent 2px, rgba(0,0,0,.2) 2px, rgba(0,0,0,.2) 3px, transparent 3px, transparent 5px, rgba(0,0,0,.2) 5px);
109 |
110 | }
111 |
--------------------------------------------------------------------------------
/playback.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Web Flow Playback
5 |
6 |
7 |
8 |
9 |
10 |
![]()
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | Message
27 |
28 |
http://goo.gl/shortlink
29 |
Press ⌘C to copy.
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/playback.js:
--------------------------------------------------------------------------------
1 | function $(id) { return document.querySelector(id); }
2 |
3 | var background = chrome.extension.getBackgroundPage();
4 | // Images from the screen capture
5 | var images = background.images;
6 | var currentIndex = 0;
7 | // Where to render the image
8 | var $image = $('#image');
9 | // Playback timer
10 | var timer = null;
11 |
12 | document.addEventListener('DOMContentLoaded', function() {
13 | // Setup listeners for rew, pp, ff and slider
14 | $('#slider').addEventListener('change', function(event) {
15 | var ratio = $('#slider').value / 100;
16 | setIndex(parseInt((images.length - 1) * ratio, 10));
17 | pause();
18 | });
19 |
20 | $('#playpause').addEventListener('click', function(event) {
21 | playpause();
22 | });
23 |
24 | $('#still').addEventListener('click', function(event) {
25 | shareStill();
26 | });
27 |
28 | $('#video').addEventListener('click', function(event) {
29 | alert('Sorry, video sharing is not implemented yet');
30 | //shareVideo();
31 | });
32 |
33 | document.addEventListener('keydown', function(event) {
34 | if (event.keyCode == 32) {
35 | playpause();
36 | }
37 | });
38 |
39 | // Set the first frame
40 | setIndex(currentIndex);
41 | // Set the state to playback
42 | setState('playback');
43 | });
44 |
45 | function updateSliderPosition() {
46 | setState('playback');
47 | var percent = parseInt(currentIndex * 100 / (images.length - 1), 10);
48 | $('#slider').value = percent;
49 | }
50 |
51 | function playpause() {
52 | if (timer) {
53 | pause();
54 | } else {
55 | play();
56 | }
57 | }
58 |
59 | function play() {
60 | // Update icon
61 | $('#playpause').className = 'pause';
62 | // If already at the end, restart
63 | if (currentIndex == images.length - 1) {
64 | setIndex(0);
65 | updateSliderPosition();
66 | }
67 | // Load images and render them in sequence
68 | timer = setInterval(function() {
69 | if (currentIndex >= images.length - 1) {
70 | pause();
71 | return;
72 | }
73 | setIndex(currentIndex + 1);
74 | updateSliderPosition();
75 | }, 1000 / background.FPS);
76 | }
77 |
78 | function pause() {
79 | $('#playpause').className = 'play';
80 | clearInterval(timer);
81 | timer = null;
82 | }
83 |
84 | function setIndex(index) {
85 | if (index >= images.length) {
86 | console.error('Index out of bounds');
87 | return;
88 | }
89 | currentIndex = index;
90 | // TODO: validate index
91 | $image.src = images[index];
92 | }
93 |
94 | function shareVideo() {
95 | setState('upload');
96 | setProgress(0);
97 | var converter = new ImagesToVideo(background.FPS);
98 | // First, upload all of the images to the server
99 | (function addImage(index) {
100 | if (index < images.length) {
101 | // While there are images left, add them to the converter
102 | var image = images[index];
103 | converter.addImage(index, image, function() {
104 | addImage(index + 1);
105 | setProgress(100 * index / images.length);
106 | });
107 | } else {
108 | setProgress(95);
109 | // Once all are gone, call the server
110 | converter.getVideo(function(data) {
111 | setSharedInfo(data.url, 'Screen capture uploaded!', '#');
112 | setState('shared');
113 | });
114 | }
115 | })(0);
116 | }
117 |
118 | function shareStill() {
119 | setState('upload');
120 | setProgress(0);
121 | // Get the current image
122 | var dataUri = images[currentIndex];
123 | setProgress(10);
124 | // Upload it
125 | getOrCreateAlbum('screenshot', function(albumId) {
126 | uploadPhoto(albumId, dataUri, function(data) {
127 | setProgress(100);
128 | console.log('upload success', data);
129 | setSharedInfo(data.url, 'Still image uploaded!');
130 | setState('shared');
131 | }, function(loaded, total) {
132 | var percent = 10 + loaded / total * 80;
133 | setProgress(percent);
134 | });
135 | });
136 | }
137 |
138 | /**
139 | * @param {String} state can be 'playback', 'upload', 'shared'
140 | */
141 | function setState(state) {
142 | var STATES = ['playback', 'upload', 'shared'];
143 | STATES.forEach(function(s) {
144 | $('#bottom .' + s).style.display = (s == state ? 'block' : 'none');
145 | });
146 | }
147 |
148 | /**
149 | * @param {Int} percent goes from 0 to 100.
150 | */
151 | function setProgress(percent) {
152 | document.querySelector('.upload .progress').style.width = percent + '%';
153 | }
154 |
155 | /**
156 | * @param {String} url the URL that was shared
157 | * @param {String} message (optional) message to the user
158 | * @param {Object} editUrl (optional) URL to edit the uploaded asset
159 | */
160 | function setSharedInfo(url, message, editUrl) {
161 | // Set link
162 | var link = $('#bottom .shared .link');
163 | link.innerText = url;
164 | // If message specified, set it
165 | if (message) {
166 | $('#bottom .shared .message').innerText = message;
167 | }
168 | // If editUrl specified, make sure user can click it
169 | if (editUrl) {
170 | }
171 | // Lastly, select the URL
172 | setTimeout(function() { selectElementContents(link); }, 200);
173 | }
174 |
175 | function selectElementContents(element) {
176 | var range = document.createRange();
177 | element.focus();
178 | range.setStart(element.firstChild, 0);
179 | range.setEnd(element.lastChild, element.innerText.length);
180 | var sel = window.getSelection();
181 | sel.removeAllRanges();
182 | sel.addRange(range);
183 | }
184 |
185 |
--------------------------------------------------------------------------------
/screencast.md:
--------------------------------------------------------------------------------
1 | Future: ScreenCast extension for screen sharing.
2 |
3 | # High level description
4 |
5 | Broadcasting:
6 | 1. Capture screenshots at some frame rate
7 | 2. Compute differences between subsequent frames (pixel-by-pixel, hashing)
8 | 3. Decide which frames are key frames (significantly different)
9 | 4. Send key frames to appengine server
10 |
11 | (V2: attempt to send diff frames)
12 |
13 | Viewing:
14 | 1. Go to the appengine server viewing page
15 | 2. Viewing page polls with XHR (or the channels API) to get new images.
16 |
17 | # Implementation
18 |
19 | Client is mostly done. Just need server.
20 |
21 | ## AppEngine server for image hosting and serving
22 |
23 | V1 API:
24 |
25 | * Create screencast with name
26 |
27 | createScreencast(name)
28 |
29 | * Upload image at time relative to video start:
30 |
31 | uploadImage(screencastId, time)
32 |
33 | * Get image at time for a given screencast:
34 |
35 | getImage(screencastId, time)
36 |
37 | Note: images can be as sparse (every 30 FPS for recordings) or as
38 | not-sparse (variable ~= 1 FPS for presentations) as you want. This
39 | allows having very dynamic videos, or just presentation-like broadcasts.
40 |
41 | ## Player for viewing 'video'
42 |
--------------------------------------------------------------------------------
/screenshot.js:
--------------------------------------------------------------------------------
1 | var FPS = 10;
2 | var QUALITY = 50;
3 |
4 | var isRecording = false;
5 | var timer = null;
6 | var images = [];
7 |
8 | function startRecording() {
9 | // Update icon to show that it's recording
10 | chrome.browserAction.setIcon({path: 'images/icon-rec.png'});
11 | chrome.browserAction.setTitle({title: 'Stop recording.'});
12 | images = [];
13 | // Set up a timer to regularly get screengrabs
14 | timer = setInterval(function() {
15 | chrome.tabs.captureVisibleTab(null, {quality: QUALITY}, function(img) {
16 | images.push(img);
17 | });
18 | }, 1000 / FPS);
19 | }
20 |
21 | function stopRecording() {
22 | // Update icon to show regular icon
23 | chrome.browserAction.setIcon({path: 'images/icon.png'});
24 | chrome.browserAction.setTitle({title: 'Start recording.'});
25 | // Stop the timer
26 | clearInterval(timer);
27 | // Playback the recorded video
28 | showVideoPlaybackPage();
29 | }
30 |
31 | function showVideoPlaybackPage() {
32 | var playbackUrl = chrome.extension.getURL('playback.html');
33 | chrome.tabs.create({url: playbackUrl});
34 | }
35 |
36 | // Listen for a click on the camera icon. On that click, take a screenshot.
37 | chrome.browserAction.onClicked.addListener(function(tab) {
38 | if (isRecording) {
39 | stopRecording();
40 | } else {
41 | startRecording();
42 | }
43 | isRecording = !isRecording;
44 | });
45 |
--------------------------------------------------------------------------------