Skip to content
Browse files

extension popup rewrite

support contentId playback and url resolution (for pass-through to
AirPlay device), adding extension popup and better flow control
  • Loading branch information...
1 parent 0e89626 commit 69fd9fb26d213dd9e790b04ecd264d30feb02e65 @benvanik committed Nov 24, 2011
View
10 README.md
@@ -64,7 +64,8 @@ Setup a new content serving request:
}
}
--> {
- id: string
+ id: string,
+ url: string
}
GET /content/[id]
@@ -194,3 +195,10 @@ TODO: Post a photo for slideshow mode:
transition: string
}
--> {}
+
+## TODO
+
+Providing a web sockets interface to status updates would prevent a lot of
+nasty polling behavior (for devices, content status, and playback status).
+There may be a way, via the /reverse command in AirPlay, to even get this
+plumbed up from the playback device itself.
View
41 apps/chrome/src/background.js
@@ -1,6 +1,19 @@
-var serviceEndpoint = 'http://localhost:8090';
+var serviceEndpoint = 'http://10.0.1.3:8090';
var browserState = new BrowserState(serviceEndpoint);
+// Route ports
+chrome.extension.onConnect.addListener(function(port) {
+ if (port.name.indexOf('popup:') == 0) {
+ // This should only ever come from the active tab - if there is no active
+ // tab then die
+ var tabId = parseInt(port.name.substr(port.name.indexOf(':') + 1));
+ var tabState = browserState.getTabState(tabId);
+ if (tabState) {
+ tabState.setPort(port);
+ }
+ }
+});
+
function getWatchUrls() {
var urls = [];
// TODO: detect from supported services/etc
@@ -82,17 +95,17 @@ chrome.experimental.webRequest.onResponseStarted.addListener(function(details) {
urls: getWatchUrls()
}, ['responseHeaders']);
-chrome.pageAction.onClicked.addListener(function(tab) {
- var device = browserState.targetDevice;
- if (!device) {
- alert('no device');
- }
+// chrome.pageAction.onClicked.addListener(function(tab) {
+// var device = browserState.targetDevice;
+// if (!device) {
+// alert('no device');
+// }
- var tabState = browserState.getTabState(tab.id);
- var video = tabState.getLastVideo();
- if (video) {
- browserState.beginPlayback(tab.id, device, video);
- } else {
- alert('no video');
- }
-});
+// var tabState = browserState.getTabState(tab.id);
+// var video = tabState.getLastVideo();
+// if (video) {
+// browserState.beginPlayback(tab.id, device, video);
+// } else {
+// alert('no video');
+// }
+// });
View
30 apps/chrome/src/base.js
@@ -5,3 +5,33 @@ function extend(childCtor, parentCtor) {
childCtor.prototype = new tempCtor();
childCtor.prototype.constructor = childCtor;
}
+
+var EventEmitter = function() {
+ this.listeners_ = {};
+};
+
+EventEmitter.prototype.addListener = function(eventName, listener) {
+ var listeners = this.listeners_[eventName];
+ if (!listeners) {
+ listeners = this.listeners_[eventName] = [];
+ }
+ listeners.push(listener);
+};
+
+EventEmitter.prototype.removeAllListeners = function() {
+ this.listeners_ = {};
+};
+
+EventEmitter.prototype.emit = function(eventName) {
+ var args = new Array(arguments.length - 1);
+ for (var n = 1; n< arguments.length; n++) {
+ args[n - 1] = arguments[n];
+ }
+ var listeners = this.listeners_[eventName];
+ if (listeners) {
+ for (var n = 0; n < listeners.length; n++) {
+ var listener = listeners[n];
+ listener.apply(window, args);
+ }
+ }
+};
View
16 apps/chrome/src/browserstate.js
@@ -77,7 +77,7 @@ BrowserState.prototype.removeDevice_ = function(device) {
BrowserState.prototype.getTabState = function(tabId) {
var tabState = this.tabStates[tabId];
if (!tabState) {
- tabState = this.tabStates[tabId] = new TabState(tabId);
+ tabState = this.tabStates[tabId] = new TabState(this, tabId);
}
return tabState;
};
@@ -87,12 +87,18 @@ BrowserState.prototype.getActiveTabState = function() {
};
BrowserState.prototype.beginPlayback = function(tabId, device, source) {
- if (this.activeTabState) {
- this.activeTabState.playbackContext.stop();
- this.activeTabState = null;
- }
+ this.endPlayback();
this.activeTabState = this.getTabState(tabId);
this.activeTabState.playbackContext =
new PlaybackContext(this, this.activeTabState, device, source);
+
+ return this.activeTabState.playbackContext;
+};
+
+BrowserState.prototype.endPlayback = function() {
+ if (this.activeTabState) {
+ this.activeTabState.playbackContext.stop();
+ this.activeTabState = null;
+ }
};
View
24 apps/chrome/src/playbackcontext.js
@@ -1,4 +1,6 @@
-var PlaybackContext = function(browser, tab, device, source) {
+var PlaybackContext = function(browser, tab, device, details) {
+ EventEmitter.call(this);
+
this.browser = browser;
this.service = browser.service;
this.tab = tab;
@@ -11,7 +13,9 @@ var PlaybackContext = function(browser, tab, device, source) {
// referer: string,
// auth: string // user:password
// }
- this.source = source;
+ this.source = {
+ content: details.url
+ };
// TODO: pull from options/etc
this.target = {
mimeType: 'video/mp4',
@@ -27,9 +31,10 @@ var PlaybackContext = function(browser, tab, device, source) {
this.prepare_();
};
+extend(PlaybackContext, EventEmitter);
// Ready polling interval, in ms
-PlaybackContext.READY_POLL_INTERVAL_ = 100;
+PlaybackContext.READY_POLL_INTERVAL_ = 500;
// Frequency of status updates, in ms
PlaybackContext.UPDATE_INTERVAL_ = 1000;
@@ -41,17 +46,17 @@ PlaybackContext.prototype.prepare_ = function() {
self.sourceInfo = info;
self.startQueryingStatus_();
- // TODO: event
+ self.emit('ready', info);
});
});
};
PlaybackContext.prototype.waitUntilReady_ = function(contentId, callback) {
var self = this;
var checkReady = function() {
- self.getContentStatus(contentId, function(status) {
+ self.service.getContentStatus(contentId, function(status) {
if (status.readyToPlay) {
- service.getContentInfo(contentId, function(info) {
+ self.service.getContentInfo(contentId, function(info) {
callback(info);
});
} else {
@@ -79,10 +84,15 @@ PlaybackContext.prototype.updateStatus_ = function() {
var self = this;
this.device.getStatus(function(status) {
self.currentStatus = status;
- // TODO: event
+ window.console.log('updating status: ' + status.position);
+ self.emit('status', status);
});
};
+PlaybackContext.prototype.play = function() {
+ this.device.play(this.service.endpoint + '/content/' + this.contentId, 0);
+};
+
PlaybackContext.prototype.seek = function(percent) {
var position = (percent / 100) * this.currentStatus.duration;
this.device.scrub(position);
View
211 apps/chrome/src/popup.html
@@ -1,18 +1,209 @@
<!DOCTYPE html>
<html>
<head>
+ <script>
+ var playButton;
+ var stopButton;
+ var rewindButton;
+
+ var seekBar;
+ var isSeeking = false;
+
+ var moreButton;
+ var moreBlock;
+ var showingMore = false;
+ var statusBox;
+ var infoBox;
+
+ var currentInfo = null;
+ var currentStatus = null;
+
+ var tabId = parseInt(window.location.search.substring(1));
+
+ var port = chrome.extension.connect({
+ name: 'popup:' + tabId
+ });
+ port.onMessage.addListener(function(msg) {
+ switch (msg.command) {
+ case 'idle':
+ playButton.innerText = '>';
+ break;
+ case 'ready':
+ if (msg.value) {
+ playButton.innerText = '||';
+ updateInfo(msg.value);
+ if (msg.status) {
+ updateStatus(msg.status);
+ }
+ } else {
+ playButton.innerText = 'X';
+ // Failed to play
+ // TODO: error display?
+ }
+ break;
+ case 'updateStatus':
+ updateStatus(msg.value);
+ break;
+ }
+ });
+
+ function formatTime(sec) {
+ var h = Math.floor(sec / 60 / 60) + ':';
+ var m = (Math.floor(sec / 60) % 60) + ':';
+ if (m.length < 3) {
+ m = '0' + m;
+ }
+ var s = (Math.floor(sec) % 60) + '';
+ if (s.length < 2) {
+ s = '0' + s;
+ }
+ var str = ((h != '0:') ? h : '') + m + s;
+ return str;
+ }
+
+ function updateInfo(info) {
+ currentInfo = info;
+
+ infoBox.innerText = JSON.stringify(info);
+ }
+
+ function updateStatus(status) {
+ currentStatus = status;
+
+ if (currentStatus) {
+ if (!isSeeking) {
+ seekBar.value = (status.position / status.duration) * 100;
+ }
+
+ if (status.rate) {
+ playButton.innerText = '||';
+ } else {
+ playButton.innerText = '>';
+ }
+
+ // TODO: better presentation
+ if (status.position !== undefined) {
+ statusBox.innerText =
+ formatTime(status.position) + '/' + formatTime(status.duration);
+ } else if (currentInfo.duration !== undefined) {
+ statusBox.innerText = formatTime(currentInfo.duration);
+ } else {
+ statusBox.innerText = '';
+ }
+ } else {
+ playButton.innerText = '>';
+
+ if (currentInfo.duration !== undefined) {
+ statusBox.innerText = formatTime(currentInfo.duration);
+ } else {
+ statusBox.innerText = '';
+ }
+ }
+ }
+
+ function play() {
+ if (playButton.innerText == '...') {
+ // Hold on...
+ return;
+ }
+
+ if (!currentStatus) {
+ port.postMessage({
+ command: 'play'
+ });
+ playButton.innerText = '...';
+ } else {
+ if (currentStatus.rate) {
+ port.postMessage({
+ command: 'pause'
+ });
+ playButton.innerText = '>';
+ } else {
+ if (currentStatus.position >= currentStatus.duration - 5) {
+ port.postMessage({
+ command: 'rewind'
+ });
+ }
+ port.postMessage({
+ command: 'resume'
+ });
+ playButton.innerText = '||';
+ }
+ }
+ }
+
+ function stop() {
+ port.postMessage({
+ command: 'stop'
+ });
+ updateStatus(null);
+ }
+
+ function rewind() {
+ port.postMessage({
+ command: 'rewind'
+ });
+ if (currentStatus && !currentStatus.rate) {
+ port.postMessage({
+ command: 'resume'
+ });
+ }
+ }
+
+ function seek(percent) {
+ port.postMessage({
+ command: 'seek',
+ value: percent
+ });
+ }
+
+ function showMore() {
+ if (showingMore) {
+ showingMore = false;
+ moreBlock.style.display = 'none';
+ moreButton.innerText = 'more';
+ } else {
+ showingMore = true;
+ moreBlock.style.display = '';
+ moreButton.innerText = 'less';
+ }
+ }
+
+ window.addEventListener('load', function() {
+ playButton = document.getElementById('playButton');
+ stopButton = document.getElementById('stopButton');
+ rewindButton = document.getElementById('rewindButton');
+
+ seekBar = document.getElementById('seekBar');
+ seekBar.addEventListener('mousedown', function() {
+ isSeeking = true;
+ });
+ seekBar.addEventListener('mouseup', function() {
+ seek(parseInt(seekBar.value));
+ isSeeking = false;
+ });
+
+ moreButton = document.getElementById('moreButton');
+ moreBlock = document.getElementById('moreBlock');
+
+ statusBox = document.getElementById('statusBox');
+ infoBox = document.getElementById('infoBox');
+ });
+ </script>
</head>
<body>
- <a href="javascript:document.getElementById('more').style.display = '';">popup</a>
- <a href="javascript:document.getElementById('more').style.display = 'none';">hide</a>
- <div id="more" style="display: none">
- content<br/>
- content<br/>
- content<br/>
- content<br/>
- content<br/>
- content<br/>
- content<br/>
+ <a id="playButton" href="javascript:play();">></a>&nbsp;
+ <a id="stopButton" href="javascript:stop();">x</a>&nbsp;
+ <a id="rewindButton" href="javascript:rewind();">|&lt;</a>&nbsp;
+ <span id="statusBox"></span>
+ <br/>
+ <input id="seekBar" type="range" min="0" max="100" value="0" step="1"/>
+ <br/>
+ <a id="moreButton" href="javascript:showMore();">more</a>
+ <div id="moreBlock" style="display: none">
+ <div id="infoBox">
+ [info]
+ </div>
</div>
</body>
</html>
View
85 apps/chrome/src/tabstate.js
@@ -1,7 +1,10 @@
-var TabState = function(tabId) {
+var TabState = function(browser, tabId) {
this.id = tabId;
+ this.browser = browser;
this.videos = [];
this.playbackContext = null;
+
+ this.port = null;
};
TabState.prototype.addVideo = function(details) {
@@ -26,11 +29,89 @@ TabState.prototype.showAction = function() {
});
chrome.pageAction.setPopup({
tabId: this.id,
- popup: '../src/popup.html'
+ popup: '../src/popup.html?' + this.id
});
chrome.pageAction.show(this.id);
};
TabState.prototype.hideAction = function() {
chrome.pageAction.hide(this.id);
};
+
+TabState.prototype.setPort = function(port) {
+ function bindContext(ctx) {
+ ctx.addListener('ready', function(info) {
+ port.postMessage({
+ command: 'ready',
+ value: info
+ });
+ ctx.play();
+ });
+ ctx.addListener('status', function(status) {
+ port.postMessage({
+ command: 'updateStatus',
+ value: status
+ });
+ });
+ };
+
+ this.port = port;
+ var self = this;
+ port.onMessage.addListener(function(msg) {
+ switch (msg.command) {
+ case 'play':
+ var device = self.browser.targetDevice;
+ if (!device) {
+ alert('no device');
+ }
+ var video = self.getLastVideo();
+ if (video) {
+ var ctx = self.browser.beginPlayback(self.id, device, video);
+ bindContext(ctx);
+ } else {
+ alert('no video');
+ }
+ break;
+ case 'resume':
+ if (self.playbackContext) {
+ self.playbackContext.resume();
+ }
+ break;
+ case 'pause':
+ if (self.playbackContext) {
+ self.playbackContext.pause();
+ }
+ break;
+ case 'stop':
+ self.browser.endPlayback();
+ break;
+ case 'rewind':
+ if (self.playbackContext) {
+ self.playbackContext.rewind();
+ }
+ break;
+ case 'seek':
+ if (self.playbackContext) {
+ self.playbackContext.seek(msg.value);
+ }
+ break;
+ }
+ });
+ port.onDisconnect.addListener(function() {
+ self.playbackContext.removeAllListeners();
+ self.port = null;
+ });
+
+ if (this.playbackContext) {
+ bindContext(this.playbackContext);
+ port.postMessage({
+ command: 'ready',
+ value: this.playbackContext.sourceInfo,
+ status: this.playbackContext.currentStatus
+ });
+ } else {
+ port.postMessage({
+ command: 'idle'
+ });
+ }
+};
View
2 apps/chrome/src/test.html
@@ -130,7 +130,7 @@
waitUntilContentReady(contentId, function(info) {
// TODO: better presentation
infoBox.innerText = JSON.stringify(info);
- play(endpoint + '/content/' + contentId, embed);
+ play(embed ? response.url : response.id, embed);
});
});
}
View
22 lib/api.js
@@ -1,5 +1,7 @@
var airplay = require('airplay');
+var dns = require('dns');
var http = require('http');
+var os = require('os');
var util = require('util');
var ContentCache = require('./contentcache').ContentCache;
@@ -8,6 +10,7 @@ var DeviceHandler = require('./devicehandler').DeviceHandler;
var API = function(port) {
var self = this;
+ this.endpoint = null; // populated in start()
this.port = port || 8090;
this.contentCache = new ContentCache();
@@ -51,8 +54,18 @@ var API = function(port) {
exports.API = API;
API.prototype.start = function() {
- this.browser.start();
- this.server.listen(this.port);
+ var self = this;
+
+ // TODO: compute the correct address based on the device address (reachable
+ // interface, etc)
+ // Right now, this just queries the first ipv4 addr of the hostname
+ dns.lookup(os.hostname(), 4, function(err, address, family) {
+ self.endpoint = 'http://' + address + ':' + self.port;
+ util.puts('service endpoint: ' + self.endpoint);
+
+ self.browser.start();
+ self.server.listen(self.port);
+ });
};
API.prototype.dispatchDeviceListRequest = function(req, requestBody, res) {
@@ -80,7 +93,7 @@ API.prototype.dispatchDeviceRequest = function(req, requestBody, res) {
if (device) {
// Setup handler, if needed
if (!device.handler) {
- device.handler = new DeviceHandler(device);
+ device.handler = new DeviceHandler(device, this.endpoint);
}
var actionName = deviceMatch[2] || 'default';
@@ -117,7 +130,8 @@ API.prototype.dispatchContentSetupRequest = function(req, requestBody, res) {
'Content-Type': 'text/plain'
});
res.end(JSON.stringify({
- id: content.id
+ id: content.id,
+ url: this.endpoint + '/content/' + content.id
}));
};
View
1 lib/content.js
@@ -142,6 +142,7 @@ Content.prototype.extractMediaInfo_ = function(callback) {
case 'https:':
this.downloadFile_(this.source.content, 0, 64000, function(filename) {
if (filename) {
+ util.puts(filename);
transcoding.queryInfo(filename, function(err, info) {
callback(err ? null : info);
});
View
7 lib/devicehandler.js
@@ -1,5 +1,6 @@
-var DeviceHandler = function(device) {
+var DeviceHandler = function(device, endpoint) {
this.device = device;
+ this.endpoint = endpoint;
};
exports.DeviceHandler = DeviceHandler;
@@ -19,6 +20,10 @@ DeviceHandler.prototype.authorize = function(req, callback) {
};
DeviceHandler.prototype.play = function(req, callback) {
+ var content = req.content;
+ if (content.indexOf(':') == -1) {
+ content = this.endpoint + '/content/' + req.contentId;
+ }
this.device.play(req.content, req.start, function(res) {
callback(res);
});

0 comments on commit 69fd9fb

Please sign in to comment.
Something went wrong with that request. Please try again.