├── .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 | Play
14 | Pause
15 | Stop
16 | Seek
17 | Seek to
18 | Next
19 |
20 |
21 |
22 | Loop mode:
23 |
24 |
25 | NO
26 | NEXT
27 | LOOP
28 | RANDOM
29 |
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 | Play
14 | Pause
15 | Stop
16 | Seek
17 | Seek to
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 | Play
14 | Pause
15 | Stop
16 | Seek
17 | Seek to
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 | Play
14 | Pause
15 | Stop
16 | Seek
17 | Seek to
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 | })();
--------------------------------------------------------------------------------