Browse files

rearranging project to be just the trampoline server

  • Loading branch information...
1 parent f9a7a2b commit bd65e2ded450d85e417f1b5a4e2329172f314c7d @benvanik committed Nov 5, 2011
View
2 .gitignore
@@ -1,2 +1,2 @@
node_modules
-
+.DS_Store
View
1 .npmignore
@@ -1,4 +1,5 @@
node_modules
+.DS_Store
.gitignore
.git/
.git*
View
312 README.md
@@ -1,180 +1,132 @@
-
-
-
-Service API:
-
- List all devices on the network (query occasionally)
- GET /device/
- --> {
- devices: [
- {
- id: string,
- name: string,
- deviceId: string,
- features: number,
- model: string,
- slideshowFeatures: [],
- supportedContentTypes: [string, ...]
- }, ...
- ]
- }
-
- Get the information of a specific device
- GET /device/id/
- --> {
- id: string,
- name: string,
- deviceId: string,
- features: number,
- model: string,
- slideshowFeatures: [],
- supportedContentTypes: [string, ...]
- }
-
- Get the playback status of a device
- GET /device/id/status
- --> {
- duration: number,
- position: number,
- rate: number,
- playbackBufferEmpty: boolean,
- playbackBufferFull: boolean,
- playbackLikelyToKeepUp: boolean,
- readyToPlay: boolean,
- loadedTimeRanges: [
- {
- start: number,
- duration: number
- }, ...
- ],
- seekableTimeRanges: [
- {
- start: number,
- duration: number
- }, ...
- ]
- }
-
- TODO: Authorize a device
- POST /device/id/authorize
- {}
- --> {}
-
- Begin playback of the given content
- POST /device/id/play
- {
- content: string,
- start: number
- }
- --> {}
-
- Stop playback of the current content
- POST /device/id/stop
- {}
- --> {}
-
- Seek to the given position in the current content
- POST /device/id/scrub
- {
- position: number
- }
- --> {}
-
- Reverse playback of the current content
- POST /device/id/reverse
- {}
- --> {}
-
- Change the playback rate of the current content (0 = pause, 1 = resume)
- POST /device/id/rate
- {
- value: number
- }
- --> {}
-
- Adjust the playback volume
- POST /device/id/volume
- {
- value: number
- }
- --> {}
-
- TODO: Post a photo for slideshow mode
- POST /device/id/photo
- {
- content: string,
- transition: string
- }
- --> {}
-
-
-
-
-
-HTTP/1.1 200 OK
-Date: Sat, 05 Nov 2011 23:20:41 GMT
-Content-Type: text/x-apple-plist+xml
-Content-Length: 820
-X-Transmit-Date: 2011-11-05T23:20:41.911377Z
-
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
- <key>duration</key>
- <real>5555.0495000000001</real>
- <key>loadedTimeRanges</key>
- <array>
- <dict>
- <key>duration</key>
- <real>5555.0495000000001</real>
- <key>start</key>
- <real>0.0</real>
- </dict>
- </array>
- <key>playbackBufferEmpty</key>
- <true/>
- <key>playbackBufferFull</key>
- <false/>
- <key>playbackLikelyToKeepUp</key>
- <true/>
- <key>position</key>
- <real>4.6269989039999997</real>
- <key>rate</key>
- <real>1</real>
- <key>readyToPlay</key>
- <true/>
- <key>seekableTimeRanges</key>
- <array>
- <dict>
- <key>duration</key>
- <real>5555.0495000000001</real>
- <key>start</key>
- <real>0.0</real>
- </dict>
- </array>
-</dict>
-</plist>
-
-
-HTTP/1.1 200 OK
-Date: Sat, 05 Nov 2011 23:34:13 GMT
-Content-Type: text/x-apple-plist+xml
-Content-Length: 427
-
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
- <key>deviceid</key>
- <string>xx:xx:xx:xx:xx:xx</string>
- <key>features</key>
- <integer>14839</integer>
- <key>model</key>
- <string>AppleTV2,1</string>
- <key>protovers</key>
- <string>1.0</string>
- <key>srcvers</key>
- <string>120.2</string>
-</dict>
-</plist>
+Trampoline -- AirPlay control service
+====================================
+
+Trampoline provides a node server that gives a RESTful API to AirPlay devices
+on the local network via
+[node-airplay](https://github.com/benvanik/node-airplay). The server can be used
+by applications on the local machine or network to discover AirPlay devices and
+control the playback of those devices with one API.
+
+### Quickstart
+
+ npm install trampoline
+
+
+### Installation
+
+### Service REST API
+
+List all devices on the network (query occasionally):
+
+ GET /device/
+ --> {
+ devices: [
+ {
+ id: string,
+ name: string,
+ deviceId: string,
+ features: number,
+ model: string,
+ slideshowFeatures: [],
+ supportedContentTypes: [string, ...]
+ }, ...
+ ]
+ }
+
+Get the information of a specific device:
+
+ GET /device/id/
+ --> {
+ id: string,
+ name: string,
+ deviceId: string,
+ features: number,
+ model: string,
+ slideshowFeatures: [],
+ supportedContentTypes: [string, ...]
+ }
+
+Get the playback status of a device:
+
+ GET /device/id/status
+ --> {
+ duration: number,
+ position: number,
+ rate: number,
+ playbackBufferEmpty: boolean,
+ playbackBufferFull: boolean,
+ playbackLikelyToKeepUp: boolean,
+ readyToPlay: boolean,
+ loadedTimeRanges: [
+ {
+ start: number,
+ duration: number
+ }, ...
+ ],
+ seekableTimeRanges: [
+ {
+ start: number,
+ duration: number
+ }, ...
+ ]
+ }
+
+TODO: Authorize a device:
+
+ POST /device/id/authorize
+ {}
+ --> {}
+
+Begin playback of the given content:
+
+ POST /device/id/play
+ {
+ content: string,
+ start: number
+ }
+ --> {}
+
+Stop playback of the current content:
+
+ POST /device/id/stop
+ {}
+ --> {}
+
+Seek to the given position in the current content:
+
+ POST /device/id/scrub
+ {
+ position: number
+ }
+ --> {}
+
+Reverse playback of the current content:
+
+ POST /device/id/reverse
+ {}
+ --> {}
+
+Change the playback rate of the current content (0 = pause, 1 = resume):
+
+ POST /device/id/rate
+ {
+ value: number
+ }
+ --> {}
+
+Adjust the playback volume:
+
+ POST /device/id/volume
+ {
+ value: number
+ }
+ --> {}
+
+TODO: Post a photo for slideshow mode:
+
+ POST /device/id/photo
+ {
+ content: string,
+ transition: string
+ }
+ --> {}
View
0 src/service/api.js → lib/api.js
File renamed without changes.
View
0 src/service/devicehandler.js → lib/devicehandler.js
File renamed without changes.
View
4 src/service/main.js → lib/server.js
@@ -1,8 +1,6 @@
#!/usr/bin/env node
-var util = require('util');
-
-var API = require('./api').API;
+var API = require('./trampoline/api').API;
var apiPort = 8090;
var httpPort = 8091;
View
17 package.json
@@ -1,26 +1,27 @@
{
- "name": "airplay",
- "description": "Apple AirPlay client library",
+ "name": "trampoline",
+ "description": "Apple AirPlay trampoline server",
"version": "0.0.1",
"author": "Ben Vanik <ben.vanik@gmail.com>",
"contributors": [],
"repository": {
"type": "git",
- "url": "git://github.com/benvanik/node-airplay.git"
+ "url": "git://github.com/benvanik/trampoline.git"
},
"keywords": [
"apple",
"mac",
"media",
- "airplay"
+ "airplay",
+ "video",
+ "server"
],
"directories": {
- "lib": "./lib/airplay"
+ "lib": "./lib/trampoline"
},
- "main": "./lib/airplay",
+ "main": "./lib/server",
"dependencies": {
- "plist": "https://github.com/benvanik/node-plist/tarball/master",
- "mdns": "https://github.com/benvanik/node_mdns/tarball/master"
+ "airplay": ">=0.0.1"
},
"scripts": {
},
View
BIN src/extension/assets/airplay.png
Deleted file not rendered
View
17 src/extension/background.html
@@ -1,17 +0,0 @@
-<!DOCTYPE html>
-<html>
- <head>
- <script src="base.js"></script>
- <script src="service.js"></script>
- <script src="device.js"></script>
- <script src="background.js"></script>
- <script>
- /**
- * NOTE: this content script requires experimental extension APIs
- * enable in chrome:flags: 'Experimental Extension APIs'
- * TODO: copy what I did in burntable to support custom services (for
- * things like YouTube/Vimeo/etc)
- */
- </script>
- </head>
-</html>
View
170 src/extension/background.js
@@ -1,170 +0,0 @@
-var service = new Service('http://localhost:8090');
-var devices = {}; // deviceId -> Device mapping
-var targetDevice = null;
-
-function queryDevices() {
- service.getDevices(function(response) {
- if (!response) {
- window.console.log('service not running, unable to query devices');
- return;
- }
- for (var n = 0; n < response.devices.length; n++) {
- var deviceInfo = response.devices[n];
- if (!devices[deviceInfo.id]) {
- var device = new Device(service, deviceInfo);
- devices[deviceInfo.id] = device;
- if (!targetDevice) {
- targetDevice = device;
- }
- window.console.log('new device:');
- window.console.log(device);
- }
- }
- // TODO: remove devices no longer present
- });
-}
-var kDeviceRefreshInterval = 5 * 1000;
-window.setInterval(queryDevices, kDeviceRefreshInterval);
-queryDevices();
-
-function getWatchUrls() {
- var urls = [];
- // TODO: detect from supported services/etc
- return urls;
-}
-
-var TabState = function(tabId) {
- this.tabId = tabId;
- this.videos = [];
-};
-TabState.prototype.addVideo = function(details) {
- this.videos.push(details);
- chrome.pageAction.setTitle({
- tabId: this.tabId,
- title: details.url
- });
-};
-TabState.prototype.getLastVideo = function() {
- if (!this.videos.length) {
- return null;
- }
- return this.videos[this.videos.length - 1];
-};
-TabState.prototype.showAction = function() {
- chrome.pageAction.setIcon({
- tabId: this.tabId,
- path: 'assets/airplay.png'
- });
- chrome.pageAction.show(this.tabId);
-};
-TabState.prototype.hideAction = function() {
- chrome.pageAction.hide(this.tabId);
-};
-
-// tabId -> TabState
-var tabStates = {};
-
-function getTabState(tabId) {
- var tabState = tabStates[tabId];
- if (!tabState) {
- tabState = tabStates[tabId] = new TabState(tabId);
- }
- return tabState;
-}
-
-// onResponseStarted does not have tabId, so track all getfile's
-// requestId -> tabId
-var getfilesMap = {};
-chrome.experimental.webRequest.onBeforeRequest.addListener(
- function(details) {
- getfilesMap[details.requestId] = details.tabId;
- return true;
- }, {
- urls: getWatchUrls()
- });
-
-function handleResponseStarted(tabId, details) {
- // TODO: blacklist some details.url ?
-
- // Extract relevant headers
- var contentType;
- for (var n = 0; n < details.responseHeaders.length; n++) {
- var header = details.responseHeaders[n];
- switch (header.name) {
- case 'Content-Type':
- contentType = header.value;
- break;
- }
- }
-
- var likelyVideo = false;
- switch (contentType) {
- case 'video/mp4':
- case 'video/mpeg':
- case 'video/quicktime':
- // Need transcoding:
- case 'video/avi':
- case 'video/x-flv':
- case 'video/x-m4v':
- case 'video/x-msvideo':
- case 'video/x-ms-asf':
- case 'video/webm':
- likelyVideo = true;
- break;
- }
- if (!likelyVideo) {
- // TODO: try checking more - like is application/octet-stream, etc
- }
-
- if (likelyVideo) {
- window.console.log('VIDEO: ' + details.url);
- window.console.log(details);
-
- var tabState = getTabState(tabId);
- tabState.addVideo(details);
- tabState.showAction();
- }
-}
-
-chrome.experimental.webRequest.onResponseStarted.addListener(
- function(details) {
- var tabId = getfilesMap[details.requestId];
- if (tabId === undefined) {
- // TODO: scan all tabs in the current window to try to find which
- // one may have made the request
- chrome.tabs.getAllInWindow(undefined, function(tabs) {
- for (var n = 0; n < tabs.length; n++) {
- var tab = tabs[n];
- if (false) {
- handleResponseStarted(tab.id, details);
- break;
- }
- }
- });
- } else {
- handleResponseStarted(tabId, details);
- }
- }, {
- urls: getWatchUrls()
- }, ['responseHeaders']);
-
-chrome.pageAction.onClicked.addListener(function(tab) {
- // if (targetDevice) {
- // targetDevice.play('http://10.0.1.17:8199/65BEB3AE-ADF1-4FE7-9367-DB1672ED4727.m4v', 0, function(response) {
- // alert('playing');
- // });
- // }
- // return;
- var tabState = getTabState(tab.id);
- var video = tabState.getLastVideo();
- if (video) {
- alert(video);
- if (targetDevice) {
- targetDevice.play(video.url, 0, function(response) {
- window.console.log(response);
- });
- }
- } else {
- alert('no video');
- }
-});
View
7 src/extension/base.js
@@ -1,7 +0,0 @@
-function extend(childCtor, parentCtor) {
- function tempCtor() {};
- tempCtor.prototype = parentCtor.prototype;
- childCtor.superClass_ = parentCtor.prototype;
- childCtor.prototype = new tempCtor();
- childCtor.prototype.constructor = childCtor;
-}
View
49 src/extension/device.js
@@ -1,49 +0,0 @@
-var Device = function(service, deviceInfo) {
- this.service = service;
- this.id = deviceInfo.id;
- this.name = deviceInfo.name;
- this.deviceId = deviceInfo.deviceId;
- this.features = deviceInfo.features;
- this.model = deviceInfo.model;
- this.slideshowFeatures = deviceInfo.slideshowFeatures;
- this.supportedContentTypes = deviceInfo.supportedContentTypes;
-
- this.status = null;
-};
-
-Device.prototype.getStatus = function(callback) {
- // TODO: cache status so a request isn't needed each time
- this.service.getDeviceStatus(this.id, callback);
-};
-
-Device.prototype.authorize = function(callback) {
- this.service.authorize(this.id, callback);
-};
-
-Device.prototype.play = function(content, start, callback) {
- this.service.play(this.id, content, start, callback);
-};
-
-Device.prototype.stop = function(callback) {
- this.service.stop(this.id, callback);
-};
-
-Device.prototype.scrub = function(position, callback) {
- this.service.scrub(this.id, position, callback);
-};
-
-Device.prototype.reverse = function(callback) {
- this.service.reverse(this.id, callback);
-};
-
-Device.prototype.rate = function(value, callback) {
- this.service.rate(this.id, value, callback);
-};
-
-Device.prototype.volume = function(value, callback) {
- this.service.volume(this.id, value, callback);
-};
-
-Device.prototype.photo = function(content, transition, callback) {
- this.service.photo(this.id, content, transition, callback);
-};
View
16 src/extension/manifest.json
@@ -1,16 +0,0 @@
-{
- "name": "Trampoline",
- "version": "0.1",
- "description": "Play web content on AirPlay devices",
- "update_url" : "https://github.com/downloads/benvanik/Trampoline/chrome-update.xml",
- "background_page": "background.html",
- "page_action": {
- "default_title": "Send the currently playing video to your AirPlay device"
- },
- "permissions": [
- "tabs",
- "experimental",
- "http://*/",
- "https://*/"
- ]
-}
View
132 src/extension/service.js
@@ -1,132 +0,0 @@
-var Service = function(endpoint, user, password) {
- this.endpoint = endpoint;
- this.user = user || null;
- this.password = password || null;
-};
-
-Service.prototype.get_ = function(path, callback) {
- var req = new XMLHttpRequest();
- req.onreadystatechange = function() {
- if (req.readyState == 4) {
- if (req.status == 200) {
- callback(JSON.parse(req.responseText));
- } else {
- callback(null);
- }
- }
- };
- req.open('GET', this.endpoint + path, true, this.user, this.password);
- req.send(null);
-};
-
-Service.prototype.post_ = function(path, requestBody, callback) {
- var req = new XMLHttpRequest();
- req.onreadystatechange = function() {
- if (req.readyState == 4) {
- if (req.status == 200) {
- callback(JSON.parse(req.responseText));
- } else {
- callback(null);
- }
- }
- };
- req.open('POST', this.endpoint + path, true, this.user, this.password);
- req.setRequestHeader('Content-Type', 'text/plain;charset=UTF-8');
- req.send(JSON.stringify(requestBody));
-};
-
-Service.prototype.getDevices = function(callback) {
- this.get_('/device/', function(response) {
- callback(response);
- });
-};
-
-Service.prototype.getDeviceInfo = function(deviceId, callback) {
- this.get_('/device/' + deviceId + '/', function(response) {
- window.console.log(response);
- callback(response);
- });
-};
-
-Service.prototype.getDeviceStatus = function(deviceId, callback) {
- this.get_('/device/' + deviceId + '/status', function(response) {
- window.console.log(response);
- callback(response);
- });
-};
-
-Service.prototype.authorize = function(deviceId, callback) {
- var request = {};
- this.post_('/device/' + deviceId + '/authorize', request, function(response) {
- window.console.log(response);
- callback(response);
- });
-};
-
-Service.prototype.play = function(deviceId, content, start, callback) {
- var request = {
- content: content,
- start: start
- };
- this.post_('/device/' + deviceId + '/play', request, function(response) {
- window.console.log(response);
- callback(response);
- });
-};
-
-Service.prototype.stop = function(deviceId, callback) {
- var request = {};
- this.post_('/device/' + deviceId + '/stop', request, function(response) {
- window.console.log(response);
- callback(response);
- });
-};
-
-Service.prototype.scrub = function(deviceId, position, callback) {
- var request = {
- position: position
- };
- this.post_('/device/' + deviceId + '/scrub', request, function(response) {
- window.console.log(response);
- callback(response);
- });
-};
-
-Service.prototype.reverse = function(deviceId, callback) {
- var request = {};
- this.post_('/device/' + deviceId + '/reverse', request, function(response) {
- window.console.log(response);
- callback(response);
- });
-};
-
-Service.prototype.rate = function(deviceId, value, callback) {
- var request = {
- value: value
- };
- this.post_('/device/' + deviceId + '/rate', request, function(response) {
- window.console.log(response);
- callback(response);
- });
-};
-
-Service.prototype.volume = function(deviceId, value, callback) {
- var request = {
- value: value
- };
- this.post_('/device/' + deviceId + '/volume', request, function(response) {
- window.console.log(response);
- callback(response);
- });
-};
-
-Service.prototype.photo = function(deviceId, content, transition, callback) {
- var request = {
- content: content,
- transition: transition
- };
- this.post_('/device/' + deviceId + '/photo', request, function(response) {
- window.console.log(response);
- callback(response);
- });
-};

0 comments on commit bd65e2d

Please sign in to comment.