├── .gitignore ├── README.md ├── assets ├── footer.txt └── header.txt ├── bower.json ├── examples ├── playlist.html ├── soundcloud.html ├── vimeo.html └── youtube.html ├── gulpfile.js ├── lib ├── model.js ├── player.js ├── playlist.js ├── soundcloud.js ├── vimeo.js └── youtube.js ├── package.json ├── polyplayer.js ├── polyplayer.min.js ├── polyplayer.vendor.min.js ├── test ├── player.html └── playlist.html └── vendor └── froogaloop.js /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components/ 2 | node_modules/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Polyplayer 2 | 3 | Polyplayer allows you to rule YouTube's, Soundcloud's and Vimeo's player using one API. 4 | 5 | ## Features 6 | * Playing, pausing, stopping 7 | * Seek to absolute or relative position 8 | * Fetch details about videos 9 | * Listen to events 10 | 11 | ## Example 12 | 13 | More examples are in `examples/`. 14 | 15 | ```html 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 59 | 60 | 61 | ``` 62 | 63 | ## API 64 | ### Player 65 | `Player` is an instance of `Backbone.Model` and has all its [functions](http://backbonejs.org/#Model) inherited. It's located under the PP namespace inside `window` (`PP.Player`). 66 | 67 | `new Player(options)` 68 | 69 | Creates a new player instance. `options` in an object: 70 | * `videoUrl` String: Video URL, e.g. http://www.youtube.com/watch?v=eKW5iugJChk, https://soundcloud.com/mashupgermany/mashup-germany-berlin-banquet, http://vimeo.com/18890266 71 | * `container` String: CSS selector string to match the parent element 72 | 73 | `Player#play()` 74 | 75 | Plays the video. This will fire a `play` event and change the state to `PLAYING`. 76 | 77 | `Player#pause()` 78 | 79 | Pauses the video. This will fire a `pause` event and change the state to `PAUSED`. 80 | 81 | `Player#stop()` 82 | 83 | Stops the video. This will fire a `stop` event and change the state to `STOPPED`. 84 | The video will start from the beginning when played. 85 | 86 | `Player#getDetails(callback)` 87 | 88 | Fetches details about the video. `callback` is a node-like callback function (`err, result`). 89 | `result` is an object containing following properties: 90 | * `title` String: The video's title 91 | * `duration` Number: Duration in ms 92 | * `createdAt` Date: Video's creating date 93 | * `thumbnails` Array: Video's thumbnail. Each element has `width`, `height` and `url` properties 94 | 95 | `Player#getCurrentPosition()` 96 | 97 | Returns the current position in ms. 98 | 99 | `Player#getState()` 100 | 101 | Returns the current state: 102 | 103 | * Player.states.LOADING: 0 104 | * Player.states.READY: 1 105 | * Player.states.PLAYING: 2 106 | * Player.states.PAUSED: 3 107 | * Player.states.FINISHED: 4 108 | * Player.states.STOPPED: 5 109 | 110 | `Player#seek(percent)` 111 | 112 | Seeks to a relative position in the video. `percent` is a number between 0 and 1, e.g. 0.5. 113 | This will fire a `playProgress` event. 114 | 115 | `Player#seekTo(ms)` 116 | 117 | Seeks to a absolute position in the video. `ms` is the number in ms to seek to, e.g. 12000. 118 | This will fire a `playProgress` event. 119 | 120 | **Events** 121 | 122 | Use Backbone.Model's `on`, `off` and `once` function to listen to events. 123 | 124 | `play` 125 | 126 | Fired when the video starts to play. 127 | 128 | `pause` 129 | 130 | Fired when the video pauses. 131 | 132 | `stop` 133 | 134 | Fired when the video stops. 135 | 136 | `finish` 137 | 138 | Fired when the video finishs to play. 139 | 140 | `playProgress` 141 | 142 | Fired continously when the video plays or seeks. 143 | 144 | `stateChange` 145 | 146 | Fired when the states changes. See `Player#getState()`. 147 | 148 | ### Playlist 149 | 150 | `Playlist` is an instance of `Backbone.Collection` and has all its [functions](http://backbonejs.org/#Collection) inherited. It's located under the PP namespace inside `window` (`PP.Playlist`). 151 | 152 | `new Player(players, options)` 153 | 154 | * `players` is an array which contains `Player`s which should be add instantly. 155 | Creates a new player instance. `options` in an object: 156 | * `container` String: CSS selector string to match the parent element in which later added players are injected. 157 | 158 | `Playlist#add(player)` 159 | 160 | Add a player to the list. `player` is either an instance of `Player` or an options object which will be passed to the `Player`'s contructor. 161 | 162 | `Playlist#getCurrentPlayer()` 163 | 164 | Returns the current player object or `null`. 165 | 166 | `Playlist#setPlayer(playerObj)` 167 | 168 | `playerObj` should be an instance on `Player` which will be set as the current player. 169 | 170 | `Playlist#setPlayerById(playerId)` 171 | 172 | Same as `setPlayer` but gets the player using its `id`. 173 | 174 | `Playlist#nextPlayer(startFromBeginning)` 175 | 176 | Set the next player in the list as the current one. If `startFromBeginning` is true it will start from the beginning again if it reaches the bottom. 177 | 178 | `Playlist#previousPlayer(startFromEnd)` 179 | 180 | Sets the previous player in the list as the current one. If `startFromEnd` is true it will start from the end again it it reaches the top. 181 | 182 | `Playlist#randomPlayer()` 183 | 184 | Sets a random player as the current player. 185 | 186 | `Playlist#loopMode` 187 | 188 | Represents the current strategy what happends when a player finishes (`finish` event is fired): 189 | 190 | * Playlist.loopModes.NO: 0 (Nothing, default) 191 | * Playlist.loopModes.NEXT: 1 (Play the next player but don't repeat the list) 192 | * Playlist.loopModes.LOOP: 2 (Play the next player and repeat the list) 193 | * Playlist.loopModes.RANDOM: 3 (Choose a random player to be next) 194 | 195 | It also exposes following functions which behave the same as `Player` does on the current player: 196 | 197 | * `Playlist#play()` 198 | * `Playlist#pause()` 199 | * `Playlist#stop()` 200 | * `Playlist#getCurrentPosition()` 201 | * `Playlist#getState()` 202 | * `Playlist#seek(percent)` 203 | 204 | **Events** 205 | 206 | Since it's a Backbone.Collection, `Playlist` triggers all events which are emitted by its models (see Player's event). 207 | Additionally it fires a `playerChange` event when a new player is set. 208 | 209 | ## Browser support 210 | 211 | Tested successfully in Chrome 31 and Firefox 26. 212 | IE 11 has problems using the YouTube iFrame API (we cannot fix this; it's YT's problem). 213 | Mobile browsers need flash or support HTML5's video and audio elements. 214 | 215 | ## Testing 216 | 217 | Use `test/player.html` and `test/playlist.html` to run tests. You'll need Flash in order to succed all tests. 218 | 219 | ## Building 220 | 221 | We use [gulp](https://github.com/wearefractal/gulp) for building and bower for dependency management: 222 | 223 | ```bash 224 | # Install gulp and bower cli 225 | npm install -g gulp bower 226 | 227 | # Fetch all dependecies 228 | npm install 229 | bower install 230 | 231 | # Build polyplayer.js 232 | gulp build 233 | 234 | # Build polyplayer.min.js 235 | gulp build-minify 236 | 237 | # Build polyplayer.vendor.min.js 238 | gulp build-vendor 239 | 240 | # Buid all 241 | gulp all 242 | ``` 243 | 244 | See `gulpfile.js` for all tasks. 245 | 246 | ## License 247 | 248 | > (MIT License) 249 | 250 | > Copyright (c) 2013 Marius maerious@gmail.com 251 | 252 | > Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 253 | 254 | > The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 255 | 256 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 257 | -------------------------------------------------------------------------------- /assets/footer.txt: -------------------------------------------------------------------------------- 1 | 2 | var PP = {}; 3 | PP.Player = Player; 4 | PP.Playlist = Playlist; 5 | 6 | window.PP = PP; 7 | 8 | })(window); -------------------------------------------------------------------------------- /assets/header.txt: -------------------------------------------------------------------------------- 1 | (function(window) { -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "polyplaver", 3 | "dependencies": { 4 | "backbone": "~1.1.0", 5 | "underscore": "~1.5.2" 6 | }, 7 | "devDependencies": { 8 | "mocha": "visionmedia/mocha#~1.14.0", 9 | "chai": "chaijs/chai#~1.8.1", 10 | "zepto": "~1.0.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/playlist.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |
...
11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | Loop mode: 23 | 24 | 30 | 31 |
32 |
33 |
34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 131 | 132 | -------------------------------------------------------------------------------- /examples/soundcloud.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |
...
11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /examples/vimeo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |
...
11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 104 | 105 | -------------------------------------------------------------------------------- /examples/youtube.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |
...
11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 103 | 104 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require("gulp"), 2 | uglify = require("gulp-uglify"), 3 | header = require("gulp-header"), 4 | footer = require("gulp-footer"), 5 | concat = require("gulp-concat"), 6 | static = require("node-static"); 7 | 8 | /** 9 | * Concat all files from ./lib into ./polyplayer.js 10 | * No vendors are included 11 | */ 12 | gulp.task("build", function() { 13 | 14 | gulp.src([ 15 | "./lib/player.js", 16 | "./lib/playlist.js", 17 | "./lib/model.js", 18 | "./lib/soundcloud.js", 19 | "./lib/youtube.js", 20 | "./lib/vimeo.js" 21 | ]) 22 | .pipe(concat("polyplayer.js")) 23 | .pipe(header({ file: "./assets/header.txt" })) 24 | .pipe(footer({ file: "./assets/footer.txt" })) 25 | .pipe(gulp.dest("./")); 26 | 27 | }); 28 | 29 | /** 30 | * Concat and minify all files from ./lib into ./polyplayer.js 31 | * No vendors are included 32 | */ 33 | gulp.task("build-minify", function() { 34 | 35 | gulp.src([ 36 | "./lib/player.js", 37 | "./lib/playlist.js", 38 | "./lib/model.js", 39 | "./lib/soundcloud.js", 40 | "./lib/youtube.js", 41 | "./lib/vimeo.js" 42 | ]) 43 | .pipe(concat("polyplayer.min.js")) 44 | .pipe(header({ file: "./assets/header.txt" })) 45 | .pipe(footer({ file: "./assets/footer.txt" })) 46 | .pipe(uglify()) 47 | .pipe(gulp.dest("./")); 48 | 49 | }); 50 | 51 | /** 52 | * Concat and minify all files from ./lib into ./polyplayer.js 53 | * All vendors (vimeo (froogaloop.js)) are included 54 | */ 55 | gulp.task("build-vendor", function() { 56 | 57 | gulp.src([ 58 | "./vendor/froogaloop.js", 59 | "./lib/player.js", 60 | "./lib/playlist.js", 61 | "./lib/model.js", 62 | "./lib/soundcloud.js", 63 | "./lib/youtube.js", 64 | "./lib/vimeo.js" 65 | ]) 66 | .pipe(concat("polyplayer.vendor.min.js")) 67 | .pipe(header({ file: "./assets/header.txt" })) 68 | .pipe(footer({ file: "./assets/footer.txt" })) 69 | .pipe(uglify()) 70 | .pipe(gulp.dest("./")); 71 | 72 | }); 73 | 74 | /** 75 | * Development task 76 | * Run `build` for each change in ./lib 77 | * Start static server on localhost:4444 78 | */ 79 | gulp.task("default", function() { 80 | 81 | var file = new static.Server(); 82 | require("http").createServer(function (request, response) { 83 | request.addListener("end", function () { 84 | file.serve(request, response, function(e, rsp) { 85 | if (e && e.status === 404) { 86 | response.writeHead(e.status, e.headers); 87 | response.end("Not Found"); 88 | } 89 | }); 90 | }).resume(); 91 | }).listen(4444); 92 | 93 | gulp.run("build"); 94 | 95 | gulp.watch("./lib/*.js", function() { 96 | gulp.run("build"); 97 | }); 98 | 99 | }); 100 | 101 | /** 102 | * Build polyplayer.js, polyplayer.min.js and polyplayer.vendor.min.js 103 | */ 104 | gulp.task("all", function() { 105 | gulp.run("build", "build-minify", "build-vendor"); 106 | }); 107 | -------------------------------------------------------------------------------- /lib/model.js: -------------------------------------------------------------------------------- 1 | var Model = Backbone.Model.extend({ 2 | 3 | defaults: { 4 | rawDetails: null, 5 | details: null, 6 | videoId: null, 7 | videoUrl: null, 8 | playerId: null, 9 | state: Player.states.LOADING, 10 | currentPosition: null, 11 | duration: null 12 | }, 13 | 14 | _setState: function(newState) { 15 | var state = Player.states[newState.toUpperCase()] 16 | this.set("state", state); 17 | this.trigger("stateChange", state); 18 | }, 19 | 20 | _setCurrentPosition: function(ms) { 21 | this.set("currentPosition", ms); 22 | this.trigger("playProgress", { 23 | currentPosition: ms, 24 | relativePosition: ms / this.get("duration") 25 | }); 26 | }, 27 | 28 | seek: function(percent) { 29 | var this_ = this; 30 | this.getDetails(function(details) { 31 | this_.seekTo(percent * details.duration); 32 | }); 33 | }, 34 | 35 | getDetails: function(cb) { 36 | if(this.get("state") === Player.states.LOADING) { 37 | this.on("ready", _.bind(this.getDetails, this, cb)); 38 | return; 39 | } 40 | 41 | var details = this.get("details") 42 | if(details !== null) { 43 | return cb(details); 44 | } 45 | 46 | var this_ = this; 47 | this._fetchDetails(function(details) { 48 | this_.set("details", details); 49 | cb(details); 50 | }); 51 | 52 | }, 53 | 54 | getCurrentPosition: function() { 55 | return this.get("currentPosition"); 56 | }, 57 | 58 | getDuration: function() { 59 | return this.get("duration"); 60 | }, 61 | 62 | getState: function() { 63 | return this.get("state"); 64 | } 65 | }); -------------------------------------------------------------------------------- /lib/player.js: -------------------------------------------------------------------------------- 1 | var Player = function Player(givenOptions) { 2 | 3 | var options = { 4 | provider: null, 5 | videoId: null, 6 | videoUrl: null, 7 | prefetchInfo: false, 8 | container: document.body 9 | }; 10 | 11 | _.extend(options, givenOptions); 12 | 13 | if(options.videoUrl) { 14 | 15 | var details = this._parseUrl(options.videoUrl); 16 | 17 | _.extend(options, details); 18 | 19 | } 20 | 21 | if(!(options.container instanceof Element)) { 22 | options.container = document.querySelector(options.container); 23 | } 24 | 25 | options.playerId = Player._generatePlayerId(); 26 | 27 | return new this._providers[options.provider](options); 28 | 29 | }; 30 | 31 | Player.prototype._providers = { 32 | "youtube": null, 33 | "vimeo": null, 34 | "soundcloud": null 35 | }; 36 | 37 | Player.prototype._parseUrl = function parseUrl(url) { 38 | 39 | var result = { 40 | provider: null, 41 | videoUrl: null, 42 | videoId: null 43 | }; 44 | 45 | try { 46 | 47 | var re = /^(https?:\/\/)?(www.)?([a-z0-9\-]+)\.[a-z]+(\/(.*))?/i.exec(url); 48 | 49 | if(re === null) { 50 | throw "Invalid url"; 51 | } 52 | 53 | var urlProvider = re[3].toLowerCase(), 54 | path = re[5] || ""; 55 | 56 | if(!(urlProvider in this._providers)) { 57 | throw "Unknown provider"; 58 | } 59 | 60 | // Set provider and nice url 61 | result.provider = urlProvider; 62 | result.videoUrl = url; 63 | 64 | if(urlProvider === "youtube") { 65 | 66 | var id = /v=([A-Za-z0-9\-_]+)/.exec(url); 67 | 68 | /** 69 | * Valid: 70 | * http://www.youtube.com/watch?v=KniyOd1kwac 71 | */ 72 | if(id === null) { 73 | throw "YouTube requires a URL containing the video ID (v)"; 74 | } 75 | 76 | result.videoId = id[1]; 77 | 78 | } else if(urlProvider === "vimeo") { 79 | 80 | /** 81 | * Valid: 82 | * http://vimeo.com/12345 83 | * Invalid: 84 | * http://vimeo.com/ 85 | * http://vimeo.com/foo 86 | * http://vimeo.com/group/supercool 87 | */ 88 | if(!/^[0-9]+$/.test(path)) { 89 | throw "Vimeo must be a numeric video url"; 90 | } 91 | 92 | result.videoId = parseInt(path, 10); 93 | 94 | } else if(urlProvider === "soundcloud") { 95 | 96 | // Don't allow sets on soundcloud 97 | if(/^[0-9a-zA-Z-_]+\/sets\/[0-9a-zA-Z-_]+$/i.test(path)) { 98 | throw "Soundcloud sets are not implemented yet"; 99 | } 100 | 101 | if(!/^[0-9a-zA-Z-_]+\/[0-9a-zA-Z-_]+$/i.test(path)) { 102 | throw "This is not a valid url to a song on Soundcloud"; 103 | } 104 | } 105 | 106 | } catch(e) { 107 | throw e; 108 | } 109 | 110 | return result; 111 | 112 | }; 113 | 114 | Player._lastPlayerId = 0; 115 | 116 | Player._generatePlayerId = function() { 117 | return "polyplayer_" + this._lastPlayerId++; 118 | }; 119 | 120 | Player.states = { 121 | LOADING: 0, 122 | READY: 1, 123 | PLAYING: 2, 124 | PAUSED: 3, 125 | FINISHED: 4, 126 | STOPPED: 5 127 | }; -------------------------------------------------------------------------------- /lib/playlist.js: -------------------------------------------------------------------------------- 1 | var Playlist = Backbone.Collection.extend({ 2 | 3 | model: function(attrs, options) { 4 | 5 | if(!attrs.container) { 6 | var el = document.createElement("div"); 7 | options.collection._container.appendChild(el); 8 | attrs.container = el; 9 | } 10 | 11 | return new Player(attrs); 12 | }, 13 | 14 | /** 15 | * Constructor 16 | * 17 | * @param {Array} models 18 | * @param {Object} options 19 | * @param {String|Element} options.container 20 | */ 21 | initialize: function(models, options) { 22 | 23 | var container = options.container || document.body; 24 | 25 | if(!(container instanceof Element)) { 26 | container = document.querySelector(container); 27 | } 28 | 29 | this._container = container; 30 | 31 | this.on("finish", this._onFinish, this); 32 | 33 | }, 34 | 35 | /** 36 | * Current loop mode used by _onFinish 37 | * Equals to Playlist.loopModes.NO 38 | * 39 | * @api public 40 | */ 41 | loopMode: 0, 42 | 43 | /** 44 | * The current player's CID 45 | * Use getCurrentPlayer to get its instance 46 | * 47 | * @api private 48 | */ 49 | _currentPlayer: null, 50 | 51 | /** 52 | * Removes old event listeners, sets the new one and listens to its events 53 | * 54 | * @param {Player} newPlayer 55 | * @param {Boolean} autoplay 56 | */ 57 | setPlayer: function(newPlayer, autoplay) { 58 | 59 | // Remove old events listeners 60 | var old = this.getCurrentPlayer(); 61 | if(old != null) { 62 | old.stop(); 63 | } 64 | 65 | // Store new player 66 | this._currentPlayer = newPlayer; 67 | 68 | // Trigget event 69 | this.trigger("playerChange", newPlayer); 70 | 71 | // Autoplay 72 | if(!!autoplay) { 73 | newPlayer.play(); 74 | } 75 | 76 | return newPlayer; 77 | 78 | }, 79 | 80 | /** 81 | * Set current player by its Id 82 | * 83 | * @param {String} newPlayerId 84 | * @param {Boolean} autoplay 85 | * @api public 86 | */ 87 | setPlayerById: function(newPlayerId, autoplay) { 88 | this.setPlayer(this.get(newPlayerId), autoplay); 89 | }, 90 | 91 | /** 92 | * Returns the current player's model 93 | * 94 | * @return {Player} 95 | * @api public 96 | */ 97 | getCurrentPlayer: function() { 98 | return this.get(this._currentPlayer); 99 | }, 100 | 101 | getOrSetCurrentPlayer: function() { 102 | var player = this.getCurrentPlayer(); 103 | if(player == null) { 104 | return this.nextPlayer(); 105 | } 106 | return player; 107 | }, 108 | 109 | /** 110 | * Player interactions 111 | */ 112 | play: function() { 113 | this.getOrSetCurrentPlayer().play(); 114 | }, 115 | 116 | pause: function() { 117 | this.getCurrentPlayer().pause(); 118 | }, 119 | 120 | stop: function() { 121 | this.getCurrentPlayer().stop(); 122 | }, 123 | 124 | seek: function(percent) { 125 | this.getCurrentPlayer().seek(percent); 126 | }, 127 | 128 | seekTo: function(ms) { 129 | this.getCurrentPlayer().seekTo(ms); 130 | }, 131 | 132 | getState: function() { 133 | return this.getCurrentPlayer().getState(); 134 | }, 135 | 136 | getCurrentPosition: function() { 137 | return this.getCurrentPlayer().getCurrentPosition(); 138 | }, 139 | 140 | getDetails: function(callback) { 141 | this.getCurrentPlayer().getDetails(callback); 142 | }, 143 | 144 | /** 145 | * Plays the next player in the list 146 | * 147 | * @param {Boolean} repeat True to start the playlist from the beginning if it ends 148 | */ 149 | nextPlayer: function(startFromBeginning) { 150 | 151 | var current = this.getCurrentPlayer(), 152 | nextIndex = 0, 153 | startFromBeginning = !!startFromBeginning; 154 | 155 | if(current !== null) { 156 | nextIndex = this.indexOf(current) + 1; 157 | } 158 | 159 | if(!startFromBeginning && nextIndex >= this.length) { 160 | return; 161 | } 162 | 163 | nextIndex = nextIndex % this.length; 164 | 165 | return this.setPlayer(this.models[nextIndex], true); 166 | }, 167 | 168 | previousPlayer: function(startFromEnd) { 169 | 170 | var current = this.getCurrentPlayer(), 171 | nextIndex = 0, 172 | startFromEnd = !!startFromEnd; 173 | 174 | if(current !== null) { 175 | nextIndex = this.indexOf(current) - 1; 176 | } 177 | 178 | if(nextIndex < 0) { 179 | if(startFromEnd) { 180 | nextIndex += this.length; 181 | } else { 182 | return; 183 | } 184 | } 185 | 186 | return this.setPlayer(this.models[nextIndex], true); 187 | 188 | }, 189 | 190 | /** 191 | * Chooses a new random player to play next 192 | */ 193 | randomPlayer: function() { 194 | 195 | return this.setPlayer(this.models[Math.floor(this.length * Math.random())], true); 196 | 197 | }, 198 | 199 | /** 200 | * Callback for "finish" triggered by the current player 201 | * 202 | * @api private 203 | */ 204 | _onFinish: function() { 205 | var loopMode = this.loopMode; 206 | 207 | switch(loopMode) { 208 | case Playlist.loopModes.NEXT: 209 | this.nextPlayer(false); 210 | break; 211 | case Playlist.loopModes.LOOP: 212 | this.nextPlayer(true); 213 | break; 214 | case Playlist.loopModes.RANDOM: 215 | this.randomPlayer(); 216 | break; 217 | } 218 | 219 | } 220 | 221 | }); 222 | 223 | Playlist.loopModes = { 224 | NO: 0, 225 | NEXT: 1, 226 | LOOP: 2, 227 | RANDOM: 3 228 | } -------------------------------------------------------------------------------- /lib/soundcloud.js: -------------------------------------------------------------------------------- 1 | Player.prototype._providers.soundcloud = Model.extend({ 2 | 3 | initialize: function(options) { 4 | 5 | this.set({ 6 | playerId: options.playerId, 7 | url: options.videoUrl, 8 | videoId: options.videoId 9 | }); 10 | 11 | var this_ = this; 12 | this_.widget = null; 13 | this._loadScript(function() { 14 | 15 | var el = document.createElement("iframe"); 16 | el.setAttribute("id", options.playerId); 17 | el.setAttribute("src", "https://w.soundcloud.com/player/?url=" + encodeURIComponent(options.videoUrl)); 18 | options.container.appendChild(el); 19 | 20 | 21 | var w = this_.widget = SC.Widget(el); 22 | w.bind(SC.Widget.Events.READY, _.bind(this_._onReady, this_)); 23 | w.bind(SC.Widget.Events.PAUSE, _.bind(this_._onPause, this_)); 24 | w.bind(SC.Widget.Events.PLAY, _.bind(this_._onPlay, this_)); 25 | w.bind(SC.Widget.Events.FINISH, _.bind(this_._onFinish, this_)); 26 | w.bind(SC.Widget.Events.PLAY_PROGRESS, _.bind(this_._onPlayProgress, this_)); 27 | 28 | }); 29 | 30 | }, 31 | 32 | play: function() { 33 | this.widget.play(); 34 | }, 35 | 36 | pause: function() { 37 | this.widget.pause(); 38 | }, 39 | 40 | seekTo: function(ms) { 41 | this.widget.seekTo(ms); 42 | this._setCurrentPosition(ms); 43 | }, 44 | 45 | stop: function() { 46 | this.pause(); 47 | this.seek(0); 48 | this._setCurrentPosition(0); 49 | 50 | // The pause stateChanged will be fired after the stopped one 51 | // A timeout will prevent this 52 | var this_ = this; 53 | setTimeout(function() { 54 | this_._setState("stopped"); 55 | this_.trigger("stop"); 56 | }, 100); 57 | }, 58 | 59 | _loadScript: function(callback) { 60 | 61 | // Soundcloud script already loaded 62 | if("SC" in window && SC) { 63 | callback(); 64 | return; 65 | } 66 | 67 | var tag = document.createElement("script"); 68 | tag.src = "https://w.soundcloud.com/player/api.js"; 69 | 70 | tag.onload = function() { 71 | callback(); 72 | }; 73 | 74 | document.body.appendChild(tag); 75 | 76 | }, 77 | 78 | _fetchDetails: function(cb) { 79 | 80 | this.widget.getCurrentSound(_.bind(function(sound) { 81 | this.set("rawDetails", sound); 82 | var details = { 83 | duration: sound.duration, 84 | id: sound.id, 85 | title: sound.title, 86 | createdAt: new Date(sound.created_at), 87 | thumbnails: [ 88 | { 89 | width: 100, 90 | height: 100, 91 | url: sound.artwork_url 92 | } 93 | ] 94 | }; 95 | 96 | this.set("details", details); 97 | 98 | cb(details); 99 | 100 | }, this)); 101 | 102 | }, 103 | 104 | /** 105 | * Event listeners for SC.Widget.bind 106 | */ 107 | _onReady: function() { 108 | var this_ = this; 109 | 110 | this.widget.getDuration(function(d) { 111 | this_.set("duration", d); 112 | this_._setState("ready"); 113 | this_.trigger("ready"); 114 | }); 115 | }, 116 | _onPause: function() { 117 | this._setState("paused"); 118 | this.trigger("pause"); 119 | }, 120 | _onPlay: function() { 121 | this._setState("playing"); 122 | this.trigger("play"); 123 | }, 124 | _onFinish: function() { 125 | this.stop(); 126 | this._setState("finished"); 127 | this.trigger("finish"); 128 | }, 129 | _onPlayProgress: function(data) { 130 | this.set("currentPosition", data.currentPosition); 131 | this.trigger("playProgress", data); 132 | } 133 | 134 | }); -------------------------------------------------------------------------------- /lib/vimeo.js: -------------------------------------------------------------------------------- 1 | Player.prototype._providers.vimeo = Model.extend({ 2 | 3 | initialize: function(options) { 4 | 5 | this.set({ 6 | playerId: options.playerId, 7 | url: options.videoUrl, 8 | videoId: options.videoId 9 | }); 10 | 11 | var iframe = document.createElement("iframe"); 12 | iframe.setAttribute("id", options.playerId); 13 | iframe.setAttribute("src", "http://player.vimeo.com/video/" + options.videoId + "?api=1&player_id=" + options.playerId); 14 | options.container.appendChild(iframe); 15 | var player = this.player = $f(iframe); 16 | 17 | var this_ = this; 18 | player.addEvent("ready", function() { 19 | 20 | player.api("getDuration", function(num) { 21 | this_.set("duration", parseInt(num * 1000)); 22 | 23 | this_._setState("ready"); 24 | this_.trigger("ready"); 25 | }); 26 | 27 | player.addEvent("playProgress", function(data) { 28 | this_.trigger("progress", { 29 | relativePosition: data.percent 30 | }); 31 | }); 32 | 33 | player.addEvent("play", function() { 34 | this_._setState("playing"); 35 | this_.trigger("play"); 36 | }); 37 | 38 | player.addEvent("pause", function() { 39 | this_._setState("paused"); 40 | this_.trigger("pause"); 41 | }); 42 | 43 | player.addEvent("finish", function() { 44 | this_.stop(); 45 | this_._setState("finished"); 46 | this_.trigger("finish"); 47 | }); 48 | 49 | player.addEvent("playProgress", function(data) { 50 | this_.set("currentPosition", data.seconds * 1000); 51 | this_.trigger("playProgress", { 52 | currentPosition: data.seconds * 1000, 53 | relativePosition: data.percent 54 | }); 55 | }); 56 | }); 57 | 58 | }, 59 | 60 | play: function() { 61 | this.player.api("play"); 62 | }, 63 | 64 | pause: function() { 65 | this.player.api("pause"); 66 | }, 67 | 68 | seekTo: function(ms) { 69 | this.player.api("seekTo", ms / 1000); 70 | this._setCurrentPosition(ms); 71 | }, 72 | 73 | stop: function() { 74 | this.pause(); 75 | this.seek(0); 76 | this._setCurrentPosition(0); 77 | 78 | this._setState("stopped"); 79 | this.trigger("stop"); 80 | }, 81 | 82 | _fetchDetails: function(cb) { 83 | 84 | var this_ = this, 85 | xhr = new XMLHttpRequest(); 86 | 87 | xhr.onload = function(ev) { 88 | var res = ev.target.responseText; 89 | try { 90 | res = JSON.parse(res)[0]; 91 | } catch(e) { 92 | throw e; 93 | } 94 | this_.set("rawDetails", res); 95 | 96 | // FF and IE can't parse dates in the format YYYY-MM-DD HH:MM:SS, e.g. 2011-01-17 16:33:58 97 | // We need to extract the data by ourselves and put them onto Date 98 | // new Date(year, month [, day, hour, minute, second, millisecond]); 99 | // Date's month arguments begins with 0 (0 = Jan, 1 = Feb, etc) 100 | var re = /(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/.exec(res.upload_date), 101 | date = new Date(re[1], re[2] - 1, re[3], re[4], re[5], re[6]); 102 | 103 | var details = { 104 | title: res.title, 105 | duration: parseInt(res.duration) * 1000, 106 | thumbnails: [ 107 | { 108 | width: 100, 109 | height: 100, 110 | url: res.thumbnail_small 111 | }, 112 | { 113 | width: 200, 114 | height: 200, 115 | url: res.thumbnail_medium 116 | }, 117 | { 118 | width: 640, 119 | height: 640, 120 | url: res.thumbnail_large 121 | } 122 | ], 123 | createdAt: date 124 | }; 125 | 126 | 127 | this_.set("details", details); 128 | cb(details); 129 | }; 130 | xhr.open("get", "http://vimeo.com/api/v2/video/" + this.get("videoId") + ".json", true); 131 | xhr.send(); 132 | 133 | } 134 | 135 | }); 136 | -------------------------------------------------------------------------------- /lib/youtube.js: -------------------------------------------------------------------------------- 1 | Player.prototype._providers.youtube = Model.extend({ 2 | 3 | initialize: function(options) { 4 | 5 | this.set({ 6 | playerId: options.playerId, 7 | url: options.videoUrl, 8 | videoId: options.videoId 9 | }); 10 | 11 | var el = document.createElement("div"); 12 | el.setAttribute("id", options.playerId); 13 | options.container.appendChild(el); 14 | 15 | var this_ = this; 16 | var callback = function() { 17 | 18 | this_.player = new YT.Player(options.playerId, { 19 | videoId: options.videoId, 20 | events: { 21 | onStateChange: function(event) { 22 | this_._setStateById(event.data); 23 | }, 24 | onReady: function() { 25 | this_._onReady(); 26 | } 27 | } 28 | }); 29 | 30 | } 31 | 32 | // Test if the youtube api is loaded 33 | if("YT" in window) { 34 | 35 | callback(); 36 | 37 | } else { 38 | // Load YT iFrame API async 39 | var tag = document.createElement("script"); 40 | tag.src = "https://www.youtube.com/iframe_api"; 41 | document.body.appendChild(tag); 42 | 43 | tag.onload = function() { 44 | YT.ready(callback); 45 | }; 46 | 47 | } 48 | 49 | this.on("play", this._setInterval); 50 | this.on("pause", this._clearInterval); 51 | this.on("finish", this._clearInterval); 52 | }, 53 | 54 | _onReady: function() { 55 | 56 | // Duration is returned in secounds but we use ms 57 | this.set("duration", this.player.getDuration() * 1000); 58 | 59 | this._setState("ready"); 60 | this.trigger("ready"); 61 | 62 | }, 63 | 64 | play: function() { 65 | // YouTube doesn't start the video from 0s 66 | // if it's stopped. 67 | // So we reset it manually 68 | if(this.get("state") === Player.states.STOPPED) { 69 | this.seekTo(0); 70 | } 71 | 72 | this.player.playVideo(); 73 | }, 74 | 75 | pause: function() { 76 | this.player.pauseVideo(); 77 | }, 78 | 79 | stop: function() { 80 | this.player.stopVideo(); 81 | this.pause(); 82 | 83 | this._setCurrentPosition(0); 84 | 85 | this._setState("stopped"); 86 | this.trigger("stop"); 87 | }, 88 | 89 | seekTo: function(ms) { 90 | this._seekValue = ms; 91 | this._setCurrentPosition(ms); 92 | 93 | if(this._seekTimeoutSet) { 94 | return; 95 | } 96 | 97 | this._seekTimeoutSet = true; 98 | var this_ = this; 99 | setTimeout(function() { 100 | var ms = this_._seekValue; 101 | 102 | this_.player.seekTo(ms / 1000, true); 103 | this_._seekTimeoutSet = false; 104 | }, 500); 105 | }, 106 | 107 | _seekValue: null, 108 | 109 | _seekTimeoutSet: false, 110 | 111 | _setInterval: function() { 112 | 113 | // Clear any previous set intervals 114 | this._clearInterval(); 115 | 116 | var this_ = this; 117 | 118 | this._intervalId = setInterval(function() { 119 | var pos = this_.player.getCurrentTime(); 120 | this_._setCurrentPosition(pos * 1000); 121 | }, 1000); 122 | }, 123 | 124 | _clearInterval: function() { 125 | clearInterval(this._intervalId); 126 | this._intervalId = null; 127 | }, 128 | 129 | _fetchDetails: function(cb) { 130 | var this_ = this, 131 | xhr = new XMLHttpRequest(); 132 | xhr.onload = function(ev) { 133 | var res = ev.target.responseText; 134 | try { 135 | res = JSON.parse(res).entry; 136 | } catch(e) { 137 | throw e; 138 | } 139 | this_.set("rawDetails", res); 140 | 141 | var details = { 142 | title: res.title.$t, 143 | duration: parseInt(res.media$group.yt$duration.seconds) * 1000, 144 | thumbnails: [], 145 | createdAt: new Date(res.published.$t) 146 | }; 147 | 148 | _.each(res.media$group.media$thumbnail, function(img) { 149 | details.thumbnails.push({ 150 | height: img.height, 151 | width: img.width, 152 | url: img.url 153 | }); 154 | }); 155 | 156 | this_.set("details", details); 157 | cb(details); 158 | }; 159 | xhr.open("get", "https://gdata.youtube.com/feeds/api/videos/" + this.get("videoId") + "?v=2&alt=json", true); 160 | xhr.send(); 161 | }, 162 | 163 | /** 164 | * This method is identically to _setState except it accepts states which are emitted by the youtube player. 165 | * It will also trigger play/pause events 166 | * 167 | * Possible values are: 168 | -1 (unstarted) 169 | 0 (ended) 170 | 1 (playing) 171 | 2 (paused) 172 | 3 (buffering) 173 | 5 (video cued) 174 | * 175 | * @param {Number} state 176 | */ 177 | _setStateById: function(ytState) { 178 | 179 | var states = { 180 | "-1": "loading", 181 | 0: "finished", 182 | 1: "playing", 183 | 2: "paused", 184 | 3: null, 185 | 5: "ready" 186 | }; 187 | 188 | var state = states[ytState]; 189 | if(state === null) return; 190 | this._setState(state); 191 | 192 | if(state === "playing") { 193 | this.trigger("play"); 194 | } else if(state == "paused") { 195 | this.trigger("pause"); 196 | } else if(state == "finished") { 197 | this.trigger("finish"); 198 | } 199 | } 200 | 201 | }); 202 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "polyplayer", 3 | "version": "0.0.0-dev", 4 | "devDependencies": { 5 | "gulp-concat": "~2.1.4", 6 | "gulp-footer": "~0.4.0", 7 | "gulp-uglify": "~0.1.0", 8 | "gulp-header": "~0.4.0", 9 | "gulp": "~3.2.1", 10 | "node-static": "~0.7.3" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /polyplayer.js: -------------------------------------------------------------------------------- 1 | (function(window) { 2 | var Player = function Player(givenOptions) { 3 | 4 | var options = { 5 | provider: null, 6 | videoId: null, 7 | videoUrl: null, 8 | prefetchInfo: false, 9 | container: document.body 10 | }; 11 | 12 | _.extend(options, givenOptions); 13 | 14 | if(options.videoUrl) { 15 | 16 | var details = this._parseUrl(options.videoUrl); 17 | 18 | _.extend(options, details); 19 | 20 | } 21 | 22 | if(!(options.container instanceof Element)) { 23 | options.container = document.querySelector(options.container); 24 | } 25 | 26 | options.playerId = Player._generatePlayerId(); 27 | 28 | return new this._providers[options.provider](options); 29 | 30 | }; 31 | 32 | Player.prototype._providers = { 33 | "youtube": null, 34 | "vimeo": null, 35 | "soundcloud": null 36 | }; 37 | 38 | Player.prototype._parseUrl = function parseUrl(url) { 39 | 40 | var result = { 41 | provider: null, 42 | videoUrl: null, 43 | videoId: null 44 | }; 45 | 46 | try { 47 | 48 | var re = /^(https?:\/\/)?(www.)?([a-z0-9\-]+)\.[a-z]+(\/(.*))?/i.exec(url); 49 | 50 | if(re === null) { 51 | throw "Invalid url"; 52 | } 53 | 54 | var urlProvider = re[3].toLowerCase(), 55 | path = re[5] || ""; 56 | 57 | if(!(urlProvider in this._providers)) { 58 | throw "Unknown provider"; 59 | } 60 | 61 | // Set provider and nice url 62 | result.provider = urlProvider; 63 | result.videoUrl = url; 64 | 65 | if(urlProvider === "youtube") { 66 | 67 | var id = /v=([A-Za-z0-9\-_]+)/.exec(url); 68 | 69 | /** 70 | * Valid: 71 | * http://www.youtube.com/watch?v=KniyOd1kwac 72 | */ 73 | if(id === null) { 74 | throw "YouTube requires a URL containing the video ID (v)"; 75 | } 76 | 77 | result.videoId = id[1]; 78 | 79 | } else if(urlProvider === "vimeo") { 80 | 81 | /** 82 | * Valid: 83 | * http://vimeo.com/12345 84 | * Invalid: 85 | * http://vimeo.com/ 86 | * http://vimeo.com/foo 87 | * http://vimeo.com/group/supercool 88 | */ 89 | if(!/^[0-9]+$/.test(path)) { 90 | throw "Vimeo must be a numeric video url"; 91 | } 92 | 93 | result.videoId = parseInt(path, 10); 94 | 95 | } else if(urlProvider === "soundcloud") { 96 | 97 | // Don't allow sets on soundcloud 98 | if(/^[0-9a-zA-Z-_]+\/sets\/[0-9a-zA-Z-_]+$/i.test(path)) { 99 | throw "Soundcloud sets are not implemented yet"; 100 | } 101 | 102 | if(!/^[0-9a-zA-Z-_]+\/[0-9a-zA-Z-_]+$/i.test(path)) { 103 | throw "This is not a valid url to a song on Soundcloud"; 104 | } 105 | } 106 | 107 | } catch(e) { 108 | throw e; 109 | } 110 | 111 | return result; 112 | 113 | }; 114 | 115 | Player._lastPlayerId = 0; 116 | 117 | Player._generatePlayerId = function() { 118 | return "polyplayer_" + this._lastPlayerId++; 119 | }; 120 | 121 | Player.states = { 122 | LOADING: 0, 123 | READY: 1, 124 | PLAYING: 2, 125 | PAUSED: 3, 126 | FINISHED: 4, 127 | STOPPED: 5 128 | }; 129 | var Playlist = Backbone.Collection.extend({ 130 | 131 | model: function(attrs, options) { 132 | 133 | if(!attrs.container) { 134 | var el = document.createElement("div"); 135 | options.collection._container.appendChild(el); 136 | attrs.container = el; 137 | } 138 | 139 | return new Player(attrs); 140 | }, 141 | 142 | /** 143 | * Constructor 144 | * 145 | * @param {Array} models 146 | * @param {Object} options 147 | * @param {String|Element} options.container 148 | */ 149 | initialize: function(models, options) { 150 | 151 | var container = options.container || document.body; 152 | 153 | if(!(container instanceof Element)) { 154 | container = document.querySelector(container); 155 | } 156 | 157 | this._container = container; 158 | 159 | this.on("finish", this._onFinish, this); 160 | 161 | }, 162 | 163 | /** 164 | * Current loop mode used by _onFinish 165 | * Equals to Playlist.loopModes.NO 166 | * 167 | * @api public 168 | */ 169 | loopMode: 0, 170 | 171 | /** 172 | * The current player's CID 173 | * Use getCurrentPlayer to get its instance 174 | * 175 | * @api private 176 | */ 177 | _currentPlayer: null, 178 | 179 | /** 180 | * Removes old event listeners, sets the new one and listens to its events 181 | * 182 | * @param {Player} newPlayer 183 | * @param {Boolean} autoplay 184 | */ 185 | setPlayer: function(newPlayer, autoplay) { 186 | 187 | // Remove old events listeners 188 | var old = this.getCurrentPlayer(); 189 | if(old != null) { 190 | old.stop(); 191 | } 192 | 193 | // Store new player 194 | this._currentPlayer = newPlayer; 195 | 196 | // Trigget event 197 | this.trigger("playerChange", newPlayer); 198 | 199 | // Autoplay 200 | if(!!autoplay) { 201 | newPlayer.play(); 202 | } 203 | 204 | return newPlayer; 205 | 206 | }, 207 | 208 | /** 209 | * Set current player by its Id 210 | * 211 | * @param {String} newPlayerId 212 | * @param {Boolean} autoplay 213 | * @api public 214 | */ 215 | setPlayerById: function(newPlayerId, autoplay) { 216 | this.setPlayer(this.get(newPlayerId), autoplay); 217 | }, 218 | 219 | /** 220 | * Returns the current player's model 221 | * 222 | * @return {Player} 223 | * @api public 224 | */ 225 | getCurrentPlayer: function() { 226 | return this.get(this._currentPlayer); 227 | }, 228 | 229 | getOrSetCurrentPlayer: function() { 230 | var player = this.getCurrentPlayer(); 231 | if(player == null) { 232 | return this.nextPlayer(); 233 | } 234 | return player; 235 | }, 236 | 237 | /** 238 | * Player interactions 239 | */ 240 | play: function() { 241 | this.getOrSetCurrentPlayer().play(); 242 | }, 243 | 244 | pause: function() { 245 | this.getCurrentPlayer().pause(); 246 | }, 247 | 248 | stop: function() { 249 | this.getCurrentPlayer().stop(); 250 | }, 251 | 252 | seek: function(percent) { 253 | this.getCurrentPlayer().seek(percent); 254 | }, 255 | 256 | seekTo: function(ms) { 257 | this.getCurrentPlayer().seekTo(ms); 258 | }, 259 | 260 | getState: function() { 261 | return this.getCurrentPlayer().getState(); 262 | }, 263 | 264 | getCurrentPosition: function() { 265 | return this.getCurrentPlayer().getCurrentPosition(); 266 | }, 267 | 268 | getDetails: function(callback) { 269 | this.getCurrentPlayer().getDetails(callback); 270 | }, 271 | 272 | /** 273 | * Plays the next player in the list 274 | * 275 | * @param {Boolean} repeat True to start the playlist from the beginning if it ends 276 | */ 277 | nextPlayer: function(startFromBeginning) { 278 | 279 | var current = this.getCurrentPlayer(), 280 | nextIndex = 0, 281 | startFromBeginning = !!startFromBeginning; 282 | 283 | if(current !== null) { 284 | nextIndex = this.indexOf(current) + 1; 285 | } 286 | 287 | if(!startFromBeginning && nextIndex >= this.length) { 288 | return; 289 | } 290 | 291 | nextIndex = nextIndex % this.length; 292 | 293 | return this.setPlayer(this.models[nextIndex], true); 294 | }, 295 | 296 | previousPlayer: function(startFromEnd) { 297 | 298 | var current = this.getCurrentPlayer(), 299 | nextIndex = 0, 300 | startFromEnd = !!startFromEnd; 301 | 302 | if(current !== null) { 303 | nextIndex = this.indexOf(current) - 1; 304 | } 305 | 306 | if(nextIndex < 0) { 307 | if(startFromEnd) { 308 | nextIndex += this.length; 309 | } else { 310 | return; 311 | } 312 | } 313 | 314 | return this.setPlayer(this.models[nextIndex], true); 315 | 316 | }, 317 | 318 | /** 319 | * Chooses a new random player to play next 320 | */ 321 | randomPlayer: function() { 322 | 323 | return this.setPlayer(this.models[Math.floor(this.length * Math.random())], true); 324 | 325 | }, 326 | 327 | /** 328 | * Callback for "finish" triggered by the current player 329 | * 330 | * @api private 331 | */ 332 | _onFinish: function() { 333 | var loopMode = this.loopMode; 334 | 335 | switch(loopMode) { 336 | case Playlist.loopModes.NEXT: 337 | this.nextPlayer(false); 338 | break; 339 | case Playlist.loopModes.LOOP: 340 | this.nextPlayer(true); 341 | break; 342 | case Playlist.loopModes.RANDOM: 343 | this.randomPlayer(); 344 | break; 345 | } 346 | 347 | } 348 | 349 | }); 350 | 351 | Playlist.loopModes = { 352 | NO: 0, 353 | NEXT: 1, 354 | LOOP: 2, 355 | RANDOM: 3 356 | } 357 | var Model = Backbone.Model.extend({ 358 | 359 | defaults: { 360 | rawDetails: null, 361 | details: null, 362 | videoId: null, 363 | videoUrl: null, 364 | playerId: null, 365 | state: Player.states.LOADING, 366 | currentPosition: null, 367 | duration: null 368 | }, 369 | 370 | _setState: function(newState) { 371 | var state = Player.states[newState.toUpperCase()] 372 | this.set("state", state); 373 | this.trigger("stateChange", state); 374 | }, 375 | 376 | _setCurrentPosition: function(ms) { 377 | this.set("currentPosition", ms); 378 | this.trigger("playProgress", { 379 | currentPosition: ms, 380 | relativePosition: ms / this.get("duration") 381 | }); 382 | }, 383 | 384 | seek: function(percent) { 385 | var this_ = this; 386 | this.getDetails(function(details) { 387 | this_.seekTo(percent * details.duration); 388 | }); 389 | }, 390 | 391 | getDetails: function(cb) { 392 | if(this.get("state") === Player.states.LOADING) { 393 | this.on("ready", _.bind(this.getDetails, this, cb)); 394 | return; 395 | } 396 | 397 | var details = this.get("details") 398 | if(details !== null) { 399 | return cb(details); 400 | } 401 | 402 | var this_ = this; 403 | this._fetchDetails(function(details) { 404 | this_.set("details", details); 405 | cb(details); 406 | }); 407 | 408 | }, 409 | 410 | getCurrentPosition: function() { 411 | return this.get("currentPosition"); 412 | }, 413 | 414 | getDuration: function() { 415 | return this.get("duration"); 416 | }, 417 | 418 | getState: function() { 419 | return this.get("state"); 420 | } 421 | }); 422 | Player.prototype._providers.soundcloud = Model.extend({ 423 | 424 | initialize: function(options) { 425 | 426 | this.set({ 427 | playerId: options.playerId, 428 | url: options.videoUrl, 429 | videoId: options.videoId 430 | }); 431 | 432 | var this_ = this; 433 | this_.widget = null; 434 | this._loadScript(function() { 435 | 436 | var el = document.createElement("iframe"); 437 | el.setAttribute("id", options.playerId); 438 | el.setAttribute("src", "https://w.soundcloud.com/player/?url=" + encodeURIComponent(options.videoUrl)); 439 | options.container.appendChild(el); 440 | 441 | 442 | var w = this_.widget = SC.Widget(el); 443 | w.bind(SC.Widget.Events.READY, _.bind(this_._onReady, this_)); 444 | w.bind(SC.Widget.Events.PAUSE, _.bind(this_._onPause, this_)); 445 | w.bind(SC.Widget.Events.PLAY, _.bind(this_._onPlay, this_)); 446 | w.bind(SC.Widget.Events.FINISH, _.bind(this_._onFinish, this_)); 447 | w.bind(SC.Widget.Events.PLAY_PROGRESS, _.bind(this_._onPlayProgress, this_)); 448 | 449 | }); 450 | 451 | }, 452 | 453 | play: function() { 454 | this.widget.play(); 455 | }, 456 | 457 | pause: function() { 458 | this.widget.pause(); 459 | }, 460 | 461 | seekTo: function(ms) { 462 | this.widget.seekTo(ms); 463 | this._setCurrentPosition(ms); 464 | }, 465 | 466 | stop: function() { 467 | this.pause(); 468 | this.seek(0); 469 | this._setCurrentPosition(0); 470 | 471 | // The pause stateChanged will be fired after the stopped one 472 | // A timeout will prevent this 473 | var this_ = this; 474 | setTimeout(function() { 475 | this_._setState("stopped"); 476 | this_.trigger("stop"); 477 | }, 100); 478 | }, 479 | 480 | _loadScript: function(callback) { 481 | 482 | // Soundcloud script already loaded 483 | if("SC" in window && SC) { 484 | callback(); 485 | return; 486 | } 487 | 488 | var tag = document.createElement("script"); 489 | tag.src = "https://w.soundcloud.com/player/api.js"; 490 | 491 | tag.onload = function() { 492 | callback(); 493 | }; 494 | 495 | document.body.appendChild(tag); 496 | 497 | }, 498 | 499 | _fetchDetails: function(cb) { 500 | 501 | this.widget.getCurrentSound(_.bind(function(sound) { 502 | this.set("rawDetails", sound); 503 | var details = { 504 | duration: sound.duration, 505 | id: sound.id, 506 | title: sound.title, 507 | createdAt: new Date(sound.created_at), 508 | thumbnails: [ 509 | { 510 | width: 100, 511 | height: 100, 512 | url: sound.artwork_url 513 | } 514 | ] 515 | }; 516 | 517 | this.set("details", details); 518 | 519 | cb(details); 520 | 521 | }, this)); 522 | 523 | }, 524 | 525 | /** 526 | * Event listeners for SC.Widget.bind 527 | */ 528 | _onReady: function() { 529 | var this_ = this; 530 | 531 | this.widget.getDuration(function(d) { 532 | this_.set("duration", d); 533 | this_._setState("ready"); 534 | this_.trigger("ready"); 535 | }); 536 | }, 537 | _onPause: function() { 538 | this._setState("paused"); 539 | this.trigger("pause"); 540 | }, 541 | _onPlay: function() { 542 | this._setState("playing"); 543 | this.trigger("play"); 544 | }, 545 | _onFinish: function() { 546 | this.stop(); 547 | this._setState("finished"); 548 | this.trigger("finish"); 549 | }, 550 | _onPlayProgress: function(data) { 551 | this.set("currentPosition", data.currentPosition); 552 | this.trigger("playProgress", data); 553 | } 554 | 555 | }); 556 | Player.prototype._providers.youtube = Model.extend({ 557 | 558 | initialize: function(options) { 559 | 560 | this.set({ 561 | playerId: options.playerId, 562 | url: options.videoUrl, 563 | videoId: options.videoId 564 | }); 565 | 566 | var el = document.createElement("div"); 567 | el.setAttribute("id", options.playerId); 568 | options.container.appendChild(el); 569 | 570 | var this_ = this; 571 | var callback = function() { 572 | 573 | this_.player = new YT.Player(options.playerId, { 574 | videoId: options.videoId, 575 | events: { 576 | onStateChange: function(event) { 577 | this_._setStateById(event.data); 578 | }, 579 | onReady: function() { 580 | this_._onReady(); 581 | } 582 | } 583 | }); 584 | 585 | } 586 | 587 | // Test if the youtube api is loaded 588 | if("YT" in window) { 589 | 590 | callback(); 591 | 592 | } else { 593 | // Load YT iFrame API async 594 | var tag = document.createElement("script"); 595 | tag.src = "https://www.youtube.com/iframe_api"; 596 | document.body.appendChild(tag); 597 | 598 | tag.onload = function() { 599 | YT.ready(callback); 600 | }; 601 | 602 | } 603 | 604 | this.on("play", this._setInterval); 605 | this.on("pause", this._clearInterval); 606 | this.on("finish", this._clearInterval); 607 | }, 608 | 609 | _onReady: function() { 610 | 611 | // Duration is returned in secounds but we use ms 612 | this.set("duration", this.player.getDuration() * 1000); 613 | 614 | this._setState("ready"); 615 | this.trigger("ready"); 616 | 617 | }, 618 | 619 | play: function() { 620 | // YouTube doesn't start the video from 0s 621 | // if it's stopped. 622 | // So we reset it manually 623 | if(this.get("state") === Player.states.STOPPED) { 624 | this.seekTo(0); 625 | } 626 | 627 | this.player.playVideo(); 628 | }, 629 | 630 | pause: function() { 631 | this.player.pauseVideo(); 632 | }, 633 | 634 | stop: function() { 635 | this.player.stopVideo(); 636 | this.pause(); 637 | 638 | this._setCurrentPosition(0); 639 | 640 | this._setState("stopped"); 641 | this.trigger("stop"); 642 | }, 643 | 644 | seekTo: function(ms) { 645 | this._seekValue = ms; 646 | this._setCurrentPosition(ms); 647 | 648 | if(this._seekTimeoutSet) { 649 | return; 650 | } 651 | 652 | this._seekTimeoutSet = true; 653 | var this_ = this; 654 | setTimeout(function() { 655 | var ms = this_._seekValue; 656 | 657 | this_.player.seekTo(ms / 1000, true); 658 | this_._seekTimeoutSet = false; 659 | }, 500); 660 | }, 661 | 662 | _seekValue: null, 663 | 664 | _seekTimeoutSet: false, 665 | 666 | _setInterval: function() { 667 | 668 | // Clear any previous set intervals 669 | this._clearInterval(); 670 | 671 | var this_ = this; 672 | 673 | this._intervalId = setInterval(function() { 674 | var pos = this_.player.getCurrentTime(); 675 | this_._setCurrentPosition(pos * 1000); 676 | }, 1000); 677 | }, 678 | 679 | _clearInterval: function() { 680 | clearInterval(this._intervalId); 681 | this._intervalId = null; 682 | }, 683 | 684 | _fetchDetails: function(cb) { 685 | var this_ = this, 686 | xhr = new XMLHttpRequest(); 687 | xhr.onload = function(ev) { 688 | var res = ev.target.responseText; 689 | try { 690 | res = JSON.parse(res).entry; 691 | } catch(e) { 692 | throw e; 693 | } 694 | this_.set("rawDetails", res); 695 | 696 | var details = { 697 | title: res.title.$t, 698 | duration: parseInt(res.media$group.yt$duration.seconds) * 1000, 699 | thumbnails: [], 700 | createdAt: new Date(res.published.$t) 701 | }; 702 | 703 | _.each(res.media$group.media$thumbnail, function(img) { 704 | details.thumbnails.push({ 705 | height: img.height, 706 | width: img.width, 707 | url: img.url 708 | }); 709 | }); 710 | 711 | this_.set("details", details); 712 | cb(details); 713 | }; 714 | xhr.open("get", "https://gdata.youtube.com/feeds/api/videos/" + this.get("videoId") + "?v=2&alt=json", true); 715 | xhr.send(); 716 | }, 717 | 718 | /** 719 | * This method is identically to _setState except it accepts states which are emitted by the youtube player. 720 | * It will also trigger play/pause events 721 | * 722 | * Possible values are: 723 | -1 (unstarted) 724 | 0 (ended) 725 | 1 (playing) 726 | 2 (paused) 727 | 3 (buffering) 728 | 5 (video cued) 729 | * 730 | * @param {Number} state 731 | */ 732 | _setStateById: function(ytState) { 733 | 734 | var states = { 735 | "-1": "loading", 736 | 0: "finished", 737 | 1: "playing", 738 | 2: "paused", 739 | 3: null, 740 | 5: "ready" 741 | }; 742 | 743 | var state = states[ytState]; 744 | if(state === null) return; 745 | this._setState(state); 746 | 747 | if(state === "playing") { 748 | this.trigger("play"); 749 | } else if(state == "paused") { 750 | this.trigger("pause"); 751 | } else if(state == "finished") { 752 | this.trigger("finish"); 753 | } 754 | } 755 | 756 | }); 757 | 758 | Player.prototype._providers.vimeo = Model.extend({ 759 | 760 | initialize: function(options) { 761 | 762 | this.set({ 763 | playerId: options.playerId, 764 | url: options.videoUrl, 765 | videoId: options.videoId 766 | }); 767 | 768 | var iframe = document.createElement("iframe"); 769 | iframe.setAttribute("id", options.playerId); 770 | iframe.setAttribute("src", "http://player.vimeo.com/video/" + options.videoId + "?api=1&player_id=" + options.playerId); 771 | options.container.appendChild(iframe); 772 | var player = this.player = $f(iframe); 773 | 774 | var this_ = this; 775 | player.addEvent("ready", function() { 776 | 777 | player.api("getDuration", function(num) { 778 | this_.set("duration", parseInt(num * 1000)); 779 | 780 | this_._setState("ready"); 781 | this_.trigger("ready"); 782 | }); 783 | 784 | player.addEvent("playProgress", function(data) { 785 | this_.trigger("progress", { 786 | relativePosition: data.percent 787 | }); 788 | }); 789 | 790 | player.addEvent("play", function() { 791 | this_._setState("playing"); 792 | this_.trigger("play"); 793 | }); 794 | 795 | player.addEvent("pause", function() { 796 | this_._setState("paused"); 797 | this_.trigger("pause"); 798 | }); 799 | 800 | player.addEvent("finish", function() { 801 | this_.stop(); 802 | this_._setState("finished"); 803 | this_.trigger("finish"); 804 | }); 805 | 806 | player.addEvent("playProgress", function(data) { 807 | this_.set("currentPosition", data.seconds * 1000); 808 | this_.trigger("playProgress", { 809 | currentPosition: data.seconds * 1000, 810 | relativePosition: data.percent 811 | }); 812 | }); 813 | }); 814 | 815 | }, 816 | 817 | play: function() { 818 | this.player.api("play"); 819 | }, 820 | 821 | pause: function() { 822 | this.player.api("pause"); 823 | }, 824 | 825 | seekTo: function(ms) { 826 | this.player.api("seekTo", ms / 1000); 827 | this._setCurrentPosition(ms); 828 | }, 829 | 830 | stop: function() { 831 | this.pause(); 832 | this.seek(0); 833 | this._setCurrentPosition(0); 834 | 835 | this._setState("stopped"); 836 | this.trigger("stop"); 837 | }, 838 | 839 | _fetchDetails: function(cb) { 840 | 841 | var this_ = this, 842 | xhr = new XMLHttpRequest(); 843 | 844 | xhr.onload = function(ev) { 845 | var res = ev.target.responseText; 846 | try { 847 | res = JSON.parse(res)[0]; 848 | } catch(e) { 849 | throw e; 850 | } 851 | this_.set("rawDetails", res); 852 | 853 | // FF and IE can't parse dates in the format YYYY-MM-DD HH:MM:SS, e.g. 2011-01-17 16:33:58 854 | // We need to extract the data by ourselves and put them onto Date 855 | // new Date(year, month [, day, hour, minute, second, millisecond]); 856 | // Date's month arguments begins with 0 (0 = Jan, 1 = Feb, etc) 857 | var re = /(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/.exec(res.upload_date), 858 | date = new Date(re[1], re[2] - 1, re[3], re[4], re[5], re[6]); 859 | 860 | var details = { 861 | title: res.title, 862 | duration: parseInt(res.duration) * 1000, 863 | thumbnails: [ 864 | { 865 | width: 100, 866 | height: 100, 867 | url: res.thumbnail_small 868 | }, 869 | { 870 | width: 200, 871 | height: 200, 872 | url: res.thumbnail_medium 873 | }, 874 | { 875 | width: 640, 876 | height: 640, 877 | url: res.thumbnail_large 878 | } 879 | ], 880 | createdAt: date 881 | }; 882 | 883 | 884 | this_.set("details", details); 885 | cb(details); 886 | }; 887 | xhr.open("get", "http://vimeo.com/api/v2/video/" + this.get("videoId") + ".json", true); 888 | xhr.send(); 889 | 890 | } 891 | 892 | }); 893 | 894 | 895 | var PP = {}; 896 | PP.Player = Player; 897 | PP.Playlist = Playlist; 898 | 899 | window.PP = PP; 900 | 901 | })(window); -------------------------------------------------------------------------------- /polyplayer.min.js: -------------------------------------------------------------------------------- 1 | !function(t){var e=function s(t){var e={provider:null,videoId:null,videoUrl:null,prefetchInfo:!1,container:document.body};if(_.extend(e,t),e.videoUrl){var i=this._parseUrl(e.videoUrl);_.extend(e,i)}return e.container instanceof Element||(e.container=document.querySelector(e.container)),e.playerId=s._generatePlayerId(),new this._providers[e.provider](e)};e.prototype._providers={youtube:null,vimeo:null,soundcloud:null},e.prototype._parseUrl=function(t){var e={provider:null,videoUrl:null,videoId:null};try{var i=/^(https?:\/\/)?(www.)?([a-z0-9\-]+)\.[a-z]+(\/(.*))?/i.exec(t);if(null===i)throw"Invalid url";var n=i[3].toLowerCase(),r=i[5]||"";if(!(n in this._providers))throw"Unknown provider";if(e.provider=n,e.videoUrl=t,"youtube"===n){var s=/v=([A-Za-z0-9\-_]+)/.exec(t);if(null===s)throw"YouTube requires a URL containing the video ID (v)";e.videoId=s[1]}else if("vimeo"===n){if(!/^[0-9]+$/.test(r))throw"Vimeo must be a numeric video url";e.videoId=parseInt(r,10)}else if("soundcloud"===n){if(/^[0-9a-zA-Z-_]+\/sets\/[0-9a-zA-Z-_]+$/i.test(r))throw"Soundcloud sets are not implemented yet";if(!/^[0-9a-zA-Z-_]+\/[0-9a-zA-Z-_]+$/i.test(r))throw"This is not a valid url to a song on Soundcloud"}}catch(o){throw o}return e},e._lastPlayerId=0,e._generatePlayerId=function(){return"polyplayer_"+this._lastPlayerId++},e.states={LOADING:0,READY:1,PLAYING:2,PAUSED:3,FINISHED:4,STOPPED:5};var i=Backbone.Collection.extend({model:function(t,i){if(!t.container){var n=document.createElement("div");i.collection._container.appendChild(n),t.container=n}return new e(t)},initialize:function(t,e){var i=e.container||document.body;i instanceof Element||(i=document.querySelector(i)),this._container=i,this.on("finish",this._onFinish,this)},loopMode:0,_currentPlayer:null,setPlayer:function(t,e){var i=this.getCurrentPlayer();return null!=i&&i.stop(),this._currentPlayer=t,this.trigger("playerChange",t),e&&t.play(),t},setPlayerById:function(t,e){this.setPlayer(this.get(t),e)},getCurrentPlayer:function(){return this.get(this._currentPlayer)},getOrSetCurrentPlayer:function(){var t=this.getCurrentPlayer();return null==t?this.nextPlayer():t},play:function(){this.getOrSetCurrentPlayer().play()},pause:function(){this.getCurrentPlayer().pause()},stop:function(){this.getCurrentPlayer().stop()},seek:function(t){this.getCurrentPlayer().seek(t)},seekTo:function(t){this.getCurrentPlayer().seekTo(t)},getState:function(){return this.getCurrentPlayer().getState()},getCurrentPosition:function(){return this.getCurrentPlayer().getCurrentPosition()},getDetails:function(t){this.getCurrentPlayer().getDetails(t)},nextPlayer:function(t){var e=this.getCurrentPlayer(),i=0,t=!!t;return null!==e&&(i=this.indexOf(e)+1),!t&&i>=this.length?void 0:(i%=this.length,this.setPlayer(this.models[i],!0))},previousPlayer:function(t){var e=this.getCurrentPlayer(),i=0,t=!!t;if(null!==e&&(i=this.indexOf(e)-1),0>i){if(!t)return;i+=this.length}return this.setPlayer(this.models[i],!0)},randomPlayer:function(){return this.setPlayer(this.models[Math.floor(this.length*Math.random())],!0)},_onFinish:function(){var t=this.loopMode;switch(t){case i.loopModes.NEXT:this.nextPlayer(!1);break;case i.loopModes.LOOP:this.nextPlayer(!0);break;case i.loopModes.RANDOM:this.randomPlayer()}}});i.loopModes={NO:0,NEXT:1,LOOP:2,RANDOM:3};var n=Backbone.Model.extend({defaults:{rawDetails:null,details:null,videoId:null,videoUrl:null,playerId:null,state:e.states.LOADING,currentPosition:null,duration:null},_setState:function(t){var i=e.states[t.toUpperCase()];this.set("state",i),this.trigger("stateChange",i)},_setCurrentPosition:function(t){this.set("currentPosition",t),this.trigger("playProgress",{currentPosition:t,relativePosition:t/this.get("duration")})},seek:function(t){var e=this;this.getDetails(function(i){e.seekTo(t*i.duration)})},getDetails:function(t){if(this.get("state")===e.states.LOADING)return this.on("ready",_.bind(this.getDetails,this,t)),void 0;var i=this.get("details");if(null!==i)return t(i);var n=this;this._fetchDetails(function(e){n.set("details",e),t(e)})},getCurrentPosition:function(){return this.get("currentPosition")},getDuration:function(){return this.get("duration")},getState:function(){return this.get("state")}});e.prototype._providers.soundcloud=n.extend({initialize:function(t){this.set({playerId:t.playerId,url:t.videoUrl,videoId:t.videoId});var e=this;e.widget=null,this._loadScript(function(){var i=document.createElement("iframe");i.setAttribute("id",t.playerId),i.setAttribute("src","https://w.soundcloud.com/player/?url="+encodeURIComponent(t.videoUrl)),t.container.appendChild(i);var n=e.widget=SC.Widget(i);n.bind(SC.Widget.Events.READY,_.bind(e._onReady,e)),n.bind(SC.Widget.Events.PAUSE,_.bind(e._onPause,e)),n.bind(SC.Widget.Events.PLAY,_.bind(e._onPlay,e)),n.bind(SC.Widget.Events.FINISH,_.bind(e._onFinish,e)),n.bind(SC.Widget.Events.PLAY_PROGRESS,_.bind(e._onPlayProgress,e))})},play:function(){this.widget.play()},pause:function(){this.widget.pause()},seekTo:function(t){this.widget.seekTo(t),this._setCurrentPosition(t)},stop:function(){this.pause(),this.seek(0),this._setCurrentPosition(0);var t=this;setTimeout(function(){t._setState("stopped"),t.trigger("stop")},100)},_loadScript:function(e){if("SC"in t&&SC)return e(),void 0;var i=document.createElement("script");i.src="https://w.soundcloud.com/player/api.js",i.onload=function(){e()},document.body.appendChild(i)},_fetchDetails:function(t){this.widget.getCurrentSound(_.bind(function(e){this.set("rawDetails",e);var i={duration:e.duration,id:e.id,title:e.title,createdAt:new Date(e.created_at),thumbnails:[{width:100,height:100,url:e.artwork_url}]};this.set("details",i),t(i)},this))},_onReady:function(){var t=this;this.widget.getDuration(function(e){t.set("duration",e),t._setState("ready"),t.trigger("ready")})},_onPause:function(){this._setState("paused"),this.trigger("pause")},_onPlay:function(){this._setState("playing"),this.trigger("play")},_onFinish:function(){this.stop(),this._setState("finished"),this.trigger("finish")},_onPlayProgress:function(t){this.set("currentPosition",t.currentPosition),this.trigger("playProgress",t)}}),e.prototype._providers.youtube=n.extend({initialize:function(e){this.set({playerId:e.playerId,url:e.videoUrl,videoId:e.videoId});var i=document.createElement("div");i.setAttribute("id",e.playerId),e.container.appendChild(i);var n=this,r=function(){n.player=new YT.Player(e.playerId,{videoId:e.videoId,events:{onStateChange:function(t){n._setStateById(t.data)},onReady:function(){n._onReady()}}})};if("YT"in t)r();else{var s=document.createElement("script");s.src="https://www.youtube.com/iframe_api",document.body.appendChild(s),s.onload=function(){YT.ready(r)}}this.on("play",this._setInterval),this.on("pause",this._clearInterval),this.on("finish",this._clearInterval)},_onReady:function(){this.set("duration",1e3*this.player.getDuration()),this._setState("ready"),this.trigger("ready")},play:function(){this.get("state")===e.states.STOPPED&&this.seekTo(0),this.player.playVideo()},pause:function(){this.player.pauseVideo()},stop:function(){this.player.stopVideo(),this.pause(),this._setCurrentPosition(0),this._setState("stopped"),this.trigger("stop")},seekTo:function(t){if(this._seekValue=t,this._setCurrentPosition(t),!this._seekTimeoutSet){this._seekTimeoutSet=!0;var e=this;setTimeout(function(){var t=e._seekValue;e.player.seekTo(t/1e3,!0),e._seekTimeoutSet=!1},500)}},_seekValue:null,_seekTimeoutSet:!1,_setInterval:function(){this._clearInterval();var t=this;this._intervalId=setInterval(function(){var e=t.player.getCurrentTime();t._setCurrentPosition(1e3*e)},1e3)},_clearInterval:function(){clearInterval(this._intervalId),this._intervalId=null},_fetchDetails:function(t){var e=this,i=new XMLHttpRequest;i.onload=function(i){var n=i.target.responseText;try{n=JSON.parse(n).entry}catch(r){throw r}e.set("rawDetails",n);var s={title:n.title.$t,duration:1e3*parseInt(n.media$group.yt$duration.seconds),thumbnails:[],createdAt:new Date(n.published.$t)};_.each(n.media$group.media$thumbnail,function(t){s.thumbnails.push({height:t.height,width:t.width,url:t.url})}),e.set("details",s),t(s)},i.open("get","https://gdata.youtube.com/feeds/api/videos/"+this.get("videoId")+"?v=2&alt=json",!0),i.send()},_setStateById:function(t){var e={"-1":"loading",0:"finished",1:"playing",2:"paused",3:null,5:"ready"},i=e[t];null!==i&&(this._setState(i),"playing"===i?this.trigger("play"):"paused"==i?this.trigger("pause"):"finished"==i&&this.trigger("finish"))}}),e.prototype._providers.vimeo=n.extend({initialize:function(t){this.set({playerId:t.playerId,url:t.videoUrl,videoId:t.videoId});var e=document.createElement("iframe");e.setAttribute("id",t.playerId),e.setAttribute("src","http://player.vimeo.com/video/"+t.videoId+"?api=1&player_id="+t.playerId),t.container.appendChild(e);var i=this.player=$f(e),n=this;i.addEvent("ready",function(){i.api("getDuration",function(t){n.set("duration",parseInt(1e3*t)),n._setState("ready"),n.trigger("ready")}),i.addEvent("playProgress",function(t){n.trigger("progress",{relativePosition:t.percent})}),i.addEvent("play",function(){n._setState("playing"),n.trigger("play")}),i.addEvent("pause",function(){n._setState("paused"),n.trigger("pause")}),i.addEvent("finish",function(){n.stop(),n._setState("finished"),n.trigger("finish")}),i.addEvent("playProgress",function(t){n.set("currentPosition",1e3*t.seconds),n.trigger("playProgress",{currentPosition:1e3*t.seconds,relativePosition:t.percent})})})},play:function(){this.player.api("play")},pause:function(){this.player.api("pause")},seekTo:function(t){this.player.api("seekTo",t/1e3),this._setCurrentPosition(t)},stop:function(){this.pause(),this.seek(0),this._setCurrentPosition(0),this._setState("stopped"),this.trigger("stop")},_fetchDetails:function(t){var e=this,i=new XMLHttpRequest;i.onload=function(i){var n=i.target.responseText;try{n=JSON.parse(n)[0]}catch(r){throw r}e.set("rawDetails",n);var s=/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/.exec(n.upload_date),o=new Date(s[1],s[2]-1,s[3],s[4],s[5],s[6]),a={title:n.title,duration:1e3*parseInt(n.duration),thumbnails:[{width:100,height:100,url:n.thumbnail_small},{width:200,height:200,url:n.thumbnail_medium},{width:640,height:640,url:n.thumbnail_large}],createdAt:o};e.set("details",a),t(a)},i.open("get","http://vimeo.com/api/v2/video/"+this.get("videoId")+".json",!0),i.send()}});var r={};r.Player=e,r.Playlist=i,t.PP=r}(window); -------------------------------------------------------------------------------- /polyplayer.vendor.min.js: -------------------------------------------------------------------------------- 1 | !function(t){var e=(function(){function e(t){return new e.fn.init(t)}function i(e,i,n){if(!n.contentWindow.postMessage)return!1;var r=n.getAttribute("src").split("?")[0],s=JSON.stringify({method:e,value:i});"//"===r.substr(0,2)&&(r=t.location.protocol+r),n.contentWindow.postMessage(s,r)}function n(t){var e,i;try{e=JSON.parse(t.data),i=e.event||e.method}catch(n){}if("ready"!=i||d||(d=!0),t.origin!=h)return!1;var r=e.value,o=e.data,a=""===a?null:e.player_id,l=s(i,a),u=[];return l?(void 0!==r&&u.push(r),o&&u.push(o),a&&u.push(a),u.length>0?l.apply(null,u):l.call()):!1}function r(t,e,i){i?(u[i]||(u[i]={}),u[i][t]=e):u[t]=e}function s(t,e){return e?u[e][t]:u[t]}function o(t,e){if(e&&u[e]){if(!u[e][t])return!1;u[e][t]=null}else{if(!u[t])return!1;u[t]=null}return!0}function a(e){"//"===e.substr(0,2)&&(e=t.location.protocol+e);for(var i=e.split("/"),n="",r=0,s=i.length;s>r&&3>r;r++)n+=i[r],2>r&&(n+="/");return n}function l(t){return!!(t&&t.constructor&&t.call&&t.apply)}var u={},d=!1,h=(Array.prototype.slice,"");return e.fn=e.prototype={element:null,init:function(t){return"string"==typeof t&&(t=document.getElementById(t)),this.element=t,h=a(this.element.getAttribute("src")),this},api:function(t,e){if(!this.element||!t)return!1;var n=this,s=n.element,o=""!==s.id?s.id:null,a=l(e)?null:e,u=l(e)?e:null;return u&&r(t,u,o),i(t,a,s),n},addEvent:function(t,e){if(!this.element)return!1;var n=this,s=n.element,o=""!==s.id?s.id:null;return r(t,e,o),"ready"!=t?i("addEventListener",t,s):"ready"==t&&d&&e.call(null,o),n},removeEvent:function(t){if(!this.element)return!1;var e=this,n=e.element,r=""!==n.id?n.id:null,s=o(t,r);"ready"!=t&&s&&i("removeEventListener",t,n)}},e.fn.init.prototype=e.fn,t.addEventListener?t.addEventListener("message",n,!1):t.attachEvent("onmessage",n),t.Froogaloop=t.$f=e}(),function s(t){var e={provider:null,videoId:null,videoUrl:null,prefetchInfo:!1,container:document.body};if(_.extend(e,t),e.videoUrl){var i=this._parseUrl(e.videoUrl);_.extend(e,i)}return e.container instanceof Element||(e.container=document.querySelector(e.container)),e.playerId=s._generatePlayerId(),new this._providers[e.provider](e)});e.prototype._providers={youtube:null,vimeo:null,soundcloud:null},e.prototype._parseUrl=function(t){var e={provider:null,videoUrl:null,videoId:null};try{var i=/^(https?:\/\/)?(www.)?([a-z0-9\-]+)\.[a-z]+(\/(.*))?/i.exec(t);if(null===i)throw"Invalid url";var n=i[3].toLowerCase(),r=i[5]||"";if(!(n in this._providers))throw"Unknown provider";if(e.provider=n,e.videoUrl=t,"youtube"===n){var s=/v=([A-Za-z0-9\-_]+)/.exec(t);if(null===s)throw"YouTube requires a URL containing the video ID (v)";e.videoId=s[1]}else if("vimeo"===n){if(!/^[0-9]+$/.test(r))throw"Vimeo must be a numeric video url";e.videoId=parseInt(r,10)}else if("soundcloud"===n){if(/^[0-9a-zA-Z-_]+\/sets\/[0-9a-zA-Z-_]+$/i.test(r))throw"Soundcloud sets are not implemented yet";if(!/^[0-9a-zA-Z-_]+\/[0-9a-zA-Z-_]+$/i.test(r))throw"This is not a valid url to a song on Soundcloud"}}catch(o){throw o}return e},e._lastPlayerId=0,e._generatePlayerId=function(){return"polyplayer_"+this._lastPlayerId++},e.states={LOADING:0,READY:1,PLAYING:2,PAUSED:3,FINISHED:4,STOPPED:5};var i=Backbone.Collection.extend({model:function(t,i){if(!t.container){var n=document.createElement("div");i.collection._container.appendChild(n),t.container=n}return new e(t)},initialize:function(t,e){var i=e.container||document.body;i instanceof Element||(i=document.querySelector(i)),this._container=i,this.on("finish",this._onFinish,this)},loopMode:0,_currentPlayer:null,setPlayer:function(t,e){var i=this.getCurrentPlayer();return null!=i&&i.stop(),this._currentPlayer=t,this.trigger("playerChange",t),e&&t.play(),t},setPlayerById:function(t,e){this.setPlayer(this.get(t),e)},getCurrentPlayer:function(){return this.get(this._currentPlayer)},getOrSetCurrentPlayer:function(){var t=this.getCurrentPlayer();return null==t?this.nextPlayer():t},play:function(){this.getOrSetCurrentPlayer().play()},pause:function(){this.getCurrentPlayer().pause()},stop:function(){this.getCurrentPlayer().stop()},seek:function(t){this.getCurrentPlayer().seek(t)},seekTo:function(t){this.getCurrentPlayer().seekTo(t)},getState:function(){return this.getCurrentPlayer().getState()},getCurrentPosition:function(){return this.getCurrentPlayer().getCurrentPosition()},getDetails:function(t){this.getCurrentPlayer().getDetails(t)},nextPlayer:function(t){var e=this.getCurrentPlayer(),i=0,t=!!t;return null!==e&&(i=this.indexOf(e)+1),!t&&i>=this.length?void 0:(i%=this.length,this.setPlayer(this.models[i],!0))},previousPlayer:function(t){var e=this.getCurrentPlayer(),i=0,t=!!t;if(null!==e&&(i=this.indexOf(e)-1),0>i){if(!t)return;i+=this.length}return this.setPlayer(this.models[i],!0)},randomPlayer:function(){return this.setPlayer(this.models[Math.floor(this.length*Math.random())],!0)},_onFinish:function(){var t=this.loopMode;switch(t){case i.loopModes.NEXT:this.nextPlayer(!1);break;case i.loopModes.LOOP:this.nextPlayer(!0);break;case i.loopModes.RANDOM:this.randomPlayer()}}});i.loopModes={NO:0,NEXT:1,LOOP:2,RANDOM:3};var n=Backbone.Model.extend({defaults:{rawDetails:null,details:null,videoId:null,videoUrl:null,playerId:null,state:e.states.LOADING,currentPosition:null,duration:null},_setState:function(t){var i=e.states[t.toUpperCase()];this.set("state",i),this.trigger("stateChange",i)},_setCurrentPosition:function(t){this.set("currentPosition",t),this.trigger("playProgress",{currentPosition:t,relativePosition:t/this.get("duration")})},seek:function(t){var e=this;this.getDetails(function(i){e.seekTo(t*i.duration)})},getDetails:function(t){if(this.get("state")===e.states.LOADING)return this.on("ready",_.bind(this.getDetails,this,t)),void 0;var i=this.get("details");if(null!==i)return t(i);var n=this;this._fetchDetails(function(e){n.set("details",e),t(e)})},getCurrentPosition:function(){return this.get("currentPosition")},getDuration:function(){return this.get("duration")},getState:function(){return this.get("state")}});e.prototype._providers.soundcloud=n.extend({initialize:function(t){this.set({playerId:t.playerId,url:t.videoUrl,videoId:t.videoId});var e=this;e.widget=null,this._loadScript(function(){var i=document.createElement("iframe");i.setAttribute("id",t.playerId),i.setAttribute("src","https://w.soundcloud.com/player/?url="+encodeURIComponent(t.videoUrl)),t.container.appendChild(i);var n=e.widget=SC.Widget(i);n.bind(SC.Widget.Events.READY,_.bind(e._onReady,e)),n.bind(SC.Widget.Events.PAUSE,_.bind(e._onPause,e)),n.bind(SC.Widget.Events.PLAY,_.bind(e._onPlay,e)),n.bind(SC.Widget.Events.FINISH,_.bind(e._onFinish,e)),n.bind(SC.Widget.Events.PLAY_PROGRESS,_.bind(e._onPlayProgress,e))})},play:function(){this.widget.play()},pause:function(){this.widget.pause()},seekTo:function(t){this.widget.seekTo(t),this._setCurrentPosition(t)},stop:function(){this.pause(),this.seek(0),this._setCurrentPosition(0);var t=this;setTimeout(function(){t._setState("stopped"),t.trigger("stop")},100)},_loadScript:function(e){if("SC"in t&&SC)return e(),void 0;var i=document.createElement("script");i.src="https://w.soundcloud.com/player/api.js",i.onload=function(){e()},document.body.appendChild(i)},_fetchDetails:function(t){this.widget.getCurrentSound(_.bind(function(e){this.set("rawDetails",e);var i={duration:e.duration,id:e.id,title:e.title,createdAt:new Date(e.created_at),thumbnails:[{width:100,height:100,url:e.artwork_url}]};this.set("details",i),t(i)},this))},_onReady:function(){var t=this;this.widget.getDuration(function(e){t.set("duration",e),t._setState("ready"),t.trigger("ready")})},_onPause:function(){this._setState("paused"),this.trigger("pause")},_onPlay:function(){this._setState("playing"),this.trigger("play")},_onFinish:function(){this.stop(),this._setState("finished"),this.trigger("finish")},_onPlayProgress:function(t){this.set("currentPosition",t.currentPosition),this.trigger("playProgress",t)}}),e.prototype._providers.youtube=n.extend({initialize:function(e){this.set({playerId:e.playerId,url:e.videoUrl,videoId:e.videoId});var i=document.createElement("div");i.setAttribute("id",e.playerId),e.container.appendChild(i);var n=this,r=function(){n.player=new YT.Player(e.playerId,{videoId:e.videoId,events:{onStateChange:function(t){n._setStateById(t.data)},onReady:function(){n._onReady()}}})};if("YT"in t)r();else{var s=document.createElement("script");s.src="https://www.youtube.com/iframe_api",document.body.appendChild(s),s.onload=function(){YT.ready(r)}}this.on("play",this._setInterval),this.on("pause",this._clearInterval),this.on("finish",this._clearInterval)},_onReady:function(){this.set("duration",1e3*this.player.getDuration()),this._setState("ready"),this.trigger("ready")},play:function(){this.get("state")===e.states.STOPPED&&this.seekTo(0),this.player.playVideo()},pause:function(){this.player.pauseVideo()},stop:function(){this.player.stopVideo(),this.pause(),this._setCurrentPosition(0),this._setState("stopped"),this.trigger("stop")},seekTo:function(t){if(this._seekValue=t,this._setCurrentPosition(t),!this._seekTimeoutSet){this._seekTimeoutSet=!0;var e=this;setTimeout(function(){var t=e._seekValue;e.player.seekTo(t/1e3,!0),e._seekTimeoutSet=!1},500)}},_seekValue:null,_seekTimeoutSet:!1,_setInterval:function(){this._clearInterval();var t=this;this._intervalId=setInterval(function(){var e=t.player.getCurrentTime();t._setCurrentPosition(1e3*e)},1e3)},_clearInterval:function(){clearInterval(this._intervalId),this._intervalId=null},_fetchDetails:function(t){var e=this,i=new XMLHttpRequest;i.onload=function(i){var n=i.target.responseText;try{n=JSON.parse(n).entry}catch(r){throw r}e.set("rawDetails",n);var s={title:n.title.$t,duration:1e3*parseInt(n.media$group.yt$duration.seconds),thumbnails:[],createdAt:new Date(n.published.$t)};_.each(n.media$group.media$thumbnail,function(t){s.thumbnails.push({height:t.height,width:t.width,url:t.url})}),e.set("details",s),t(s)},i.open("get","https://gdata.youtube.com/feeds/api/videos/"+this.get("videoId")+"?v=2&alt=json",!0),i.send()},_setStateById:function(t){var e={"-1":"loading",0:"finished",1:"playing",2:"paused",3:null,5:"ready"},i=e[t];null!==i&&(this._setState(i),"playing"===i?this.trigger("play"):"paused"==i?this.trigger("pause"):"finished"==i&&this.trigger("finish"))}}),e.prototype._providers.vimeo=n.extend({initialize:function(t){this.set({playerId:t.playerId,url:t.videoUrl,videoId:t.videoId});var e=document.createElement("iframe");e.setAttribute("id",t.playerId),e.setAttribute("src","http://player.vimeo.com/video/"+t.videoId+"?api=1&player_id="+t.playerId),t.container.appendChild(e);var i=this.player=$f(e),n=this;i.addEvent("ready",function(){i.api("getDuration",function(t){n.set("duration",parseInt(1e3*t)),n._setState("ready"),n.trigger("ready")}),i.addEvent("playProgress",function(t){n.trigger("progress",{relativePosition:t.percent})}),i.addEvent("play",function(){n._setState("playing"),n.trigger("play")}),i.addEvent("pause",function(){n._setState("paused"),n.trigger("pause")}),i.addEvent("finish",function(){n.stop(),n._setState("finished"),n.trigger("finish")}),i.addEvent("playProgress",function(t){n.set("currentPosition",1e3*t.seconds),n.trigger("playProgress",{currentPosition:1e3*t.seconds,relativePosition:t.percent})})})},play:function(){this.player.api("play")},pause:function(){this.player.api("pause")},seekTo:function(t){this.player.api("seekTo",t/1e3),this._setCurrentPosition(t)},stop:function(){this.pause(),this.seek(0),this._setCurrentPosition(0),this._setState("stopped"),this.trigger("stop")},_fetchDetails:function(t){var e=this,i=new XMLHttpRequest;i.onload=function(i){var n=i.target.responseText;try{n=JSON.parse(n)[0]}catch(r){throw r}e.set("rawDetails",n);var s=/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/.exec(n.upload_date),o=new Date(s[1],s[2]-1,s[3],s[4],s[5],s[6]),a={title:n.title,duration:1e3*parseInt(n.duration),thumbnails:[{width:100,height:100,url:n.thumbnail_small},{width:200,height:200,url:n.thumbnail_medium},{width:640,height:640,url:n.thumbnail_large}],createdAt:o};e.set("details",a),t(a)},i.open("get","http://vimeo.com/api/v2/video/"+this.get("videoId")+".json",!0),i.send()}});var r={};r.Player=e,r.Playlist=i,t.PP=r}(window); -------------------------------------------------------------------------------- /test/player.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 337 | 338 | 339 | -------------------------------------------------------------------------------- /test/playlist.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 |
12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 286 | 287 | 288 | -------------------------------------------------------------------------------- /vendor/froogaloop.js: -------------------------------------------------------------------------------- 1 | // Init style shamelessly stolen from jQuery http://jquery.com 2 | var Froogaloop = (function(){ 3 | // Define a local copy of Froogaloop 4 | function Froogaloop(iframe) { 5 | // The Froogaloop object is actually just the init constructor 6 | return new Froogaloop.fn.init(iframe); 7 | } 8 | 9 | var eventCallbacks = {}, 10 | hasWindowEvent = false, 11 | isReady = false, 12 | slice = Array.prototype.slice, 13 | playerDomain = ''; 14 | 15 | Froogaloop.fn = Froogaloop.prototype = { 16 | element: null, 17 | 18 | init: function(iframe) { 19 | if (typeof iframe === "string") { 20 | iframe = document.getElementById(iframe); 21 | } 22 | 23 | this.element = iframe; 24 | 25 | // Register message event listeners 26 | playerDomain = getDomainFromUrl(this.element.getAttribute('src')); 27 | 28 | return this; 29 | }, 30 | 31 | /* 32 | * Calls a function to act upon the player. 33 | * 34 | * @param {string} method The name of the Javascript API method to call. Eg: 'play'. 35 | * @param {Array|Function} valueOrCallback params Array of parameters to pass when calling an API method 36 | * or callback function when the method returns a value. 37 | */ 38 | api: function(method, valueOrCallback) { 39 | if (!this.element || !method) { 40 | return false; 41 | } 42 | 43 | var self = this, 44 | element = self.element, 45 | target_id = element.id !== '' ? element.id : null, 46 | params = !isFunction(valueOrCallback) ? valueOrCallback : null, 47 | callback = isFunction(valueOrCallback) ? valueOrCallback : null; 48 | 49 | // Store the callback for get functions 50 | if (callback) { 51 | storeCallback(method, callback, target_id); 52 | } 53 | 54 | postMessage(method, params, element); 55 | return self; 56 | }, 57 | 58 | /* 59 | * Registers an event listener and a callback function that gets called when the event fires. 60 | * 61 | * @param eventName (String): Name of the event to listen for. 62 | * @param callback (Function): Function that should be called when the event fires. 63 | */ 64 | addEvent: function(eventName, callback) { 65 | if (!this.element) { 66 | return false; 67 | } 68 | 69 | var self = this, 70 | element = self.element, 71 | target_id = element.id !== '' ? element.id : null; 72 | 73 | 74 | storeCallback(eventName, callback, target_id); 75 | 76 | // The ready event is not registered via postMessage. It fires regardless. 77 | if (eventName != 'ready') { 78 | postMessage('addEventListener', eventName, element); 79 | } 80 | else if (eventName == 'ready' && isReady) { 81 | callback.call(null, target_id); 82 | } 83 | 84 | return self; 85 | }, 86 | 87 | /* 88 | * Unregisters an event listener that gets called when the event fires. 89 | * 90 | * @param eventName (String): Name of the event to stop listening for. 91 | */ 92 | removeEvent: function(eventName) { 93 | if (!this.element) { 94 | return false; 95 | } 96 | 97 | var self = this, 98 | element = self.element, 99 | target_id = element.id !== '' ? element.id : null, 100 | removed = removeCallback(eventName, target_id); 101 | 102 | // The ready event is not registered 103 | if (eventName != 'ready' && removed) { 104 | postMessage('removeEventListener', eventName, element); 105 | } 106 | } 107 | }; 108 | 109 | /** 110 | * Handles posting a message to the parent window. 111 | * 112 | * @param method (String): name of the method to call inside the player. For api calls 113 | * this is the name of the api method (api_play or api_pause) while for events this method 114 | * is api_addEventListener. 115 | * @param params (Object or Array): List of parameters to submit to the method. Can be either 116 | * a single param or an array list of parameters. 117 | * @param target (HTMLElement): Target iframe to post the message to. 118 | */ 119 | function postMessage(method, params, target) { 120 | if (!target.contentWindow.postMessage) { 121 | return false; 122 | } 123 | 124 | var url = target.getAttribute('src').split('?')[0], 125 | data = JSON.stringify({ 126 | method: method, 127 | value: params 128 | }); 129 | 130 | if (url.substr(0, 2) === '//') { 131 | url = window.location.protocol + url; 132 | } 133 | 134 | target.contentWindow.postMessage(data, url); 135 | } 136 | 137 | /** 138 | * Event that fires whenever the window receives a message from its parent 139 | * via window.postMessage. 140 | */ 141 | function onMessageReceived(event) { 142 | var data, method; 143 | 144 | try { 145 | data = JSON.parse(event.data); 146 | method = data.event || data.method; 147 | } 148 | catch(e) { 149 | //fail silently... like a ninja! 150 | } 151 | 152 | if (method == 'ready' && !isReady) { 153 | isReady = true; 154 | } 155 | 156 | // Handles messages from moogaloop only 157 | if (event.origin != playerDomain) { 158 | return false; 159 | } 160 | 161 | var value = data.value, 162 | eventData = data.data, 163 | target_id = target_id === '' ? null : data.player_id, 164 | 165 | callback = getCallback(method, target_id), 166 | params = []; 167 | 168 | if (!callback) { 169 | return false; 170 | } 171 | 172 | if (value !== undefined) { 173 | params.push(value); 174 | } 175 | 176 | if (eventData) { 177 | params.push(eventData); 178 | } 179 | 180 | if (target_id) { 181 | params.push(target_id); 182 | } 183 | 184 | return params.length > 0 ? callback.apply(null, params) : callback.call(); 185 | } 186 | 187 | 188 | /** 189 | * Stores submitted callbacks for each iframe being tracked and each 190 | * event for that iframe. 191 | * 192 | * @param eventName (String): Name of the event. Eg. api_onPlay 193 | * @param callback (Function): Function that should get executed when the 194 | * event is fired. 195 | * @param target_id (String) [Optional]: If handling more than one iframe then 196 | * it stores the different callbacks for different iframes based on the iframe's 197 | * id. 198 | */ 199 | function storeCallback(eventName, callback, target_id) { 200 | if (target_id) { 201 | if (!eventCallbacks[target_id]) { 202 | eventCallbacks[target_id] = {}; 203 | } 204 | eventCallbacks[target_id][eventName] = callback; 205 | } 206 | else { 207 | eventCallbacks[eventName] = callback; 208 | } 209 | } 210 | 211 | /** 212 | * Retrieves stored callbacks. 213 | */ 214 | function getCallback(eventName, target_id) { 215 | if (target_id) { 216 | return eventCallbacks[target_id][eventName]; 217 | } 218 | else { 219 | return eventCallbacks[eventName]; 220 | } 221 | } 222 | 223 | function removeCallback(eventName, target_id) { 224 | if (target_id && eventCallbacks[target_id]) { 225 | if (!eventCallbacks[target_id][eventName]) { 226 | return false; 227 | } 228 | eventCallbacks[target_id][eventName] = null; 229 | } 230 | else { 231 | if (!eventCallbacks[eventName]) { 232 | return false; 233 | } 234 | eventCallbacks[eventName] = null; 235 | } 236 | 237 | return true; 238 | } 239 | 240 | /** 241 | * Returns a domain's root domain. 242 | * Eg. returns http://vimeo.com when http://vimeo.com/channels is sbumitted 243 | * 244 | * @param url (String): Url to test against. 245 | * @return url (String): Root domain of submitted url 246 | */ 247 | function getDomainFromUrl(url) { 248 | if (url.substr(0, 2) === '//') { 249 | url = window.location.protocol + url; 250 | } 251 | 252 | var url_pieces = url.split('/'), 253 | domain_str = ''; 254 | 255 | for(var i = 0, length = url_pieces.length; i < length; i++) { 256 | if(i<3) {domain_str += url_pieces[i];} 257 | else {break;} 258 | if(i<2) {domain_str += '/';} 259 | } 260 | 261 | return domain_str; 262 | } 263 | 264 | function isFunction(obj) { 265 | return !!(obj && obj.constructor && obj.call && obj.apply); 266 | } 267 | 268 | function isArray(obj) { 269 | return toString.call(obj) === '[object Array]'; 270 | } 271 | 272 | // Give the init function the Froogaloop prototype for later instantiation 273 | Froogaloop.fn.init.prototype = Froogaloop.fn; 274 | 275 | // Listens for the message event. 276 | // W3C 277 | if (window.addEventListener) { 278 | window.addEventListener('message', onMessageReceived, false); 279 | } 280 | // IE 281 | else { 282 | window.attachEvent('onmessage', onMessageReceived); 283 | } 284 | 285 | // Expose froogaloop to the global object 286 | return (window.Froogaloop = window.$f = Froogaloop); 287 | 288 | })(); --------------------------------------------------------------------------------