jukebox-rails 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in jukebox-rails.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Alif Rachmawadi
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,29 @@
1
+ # Jukebox::Rails
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'jukebox-rails'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install jukebox-rails
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -0,0 +1,17 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/jukebox-rails/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Alif Rachmawadi"]
6
+ gem.email = ["subosito@gmail.com"]
7
+ gem.description = %q{Zynga's Jukebox: Sophisticated audio playback for the web.}
8
+ gem.summary = %q{The Jukebox is a component for playing sounds and music with the usage of sprites with a special focus on performance and cross-device deployment.}
9
+ gem.homepage = "https://github.com/subosito/jukebox-rails"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "jukebox-rails"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Jukebox::Rails::VERSION
17
+ end
@@ -0,0 +1,8 @@
1
+ require "jukebox-rails/version"
2
+
3
+ module Jukebox
4
+ module Rails
5
+ class Engine < ::Rails::Engine
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,5 @@
1
+ module Jukebox
2
+ module Rails
3
+ VERSION = "1.0.0"
4
+ end
5
+ end
@@ -0,0 +1,493 @@
1
+ /*
2
+ * Jukebox
3
+ * http://github.com/zynga/jukebox
4
+ *
5
+ * Copyright 2011, Zynga Inc.
6
+ * Developed by Christoph Martens (@martensms)
7
+ *
8
+ * Licensed under the MIT License.
9
+ * https://raw.github.com/zynga/jukebox/master/MIT-LICENSE.txt
10
+ *
11
+ */
12
+
13
+ if (this.jukebox === undefined) {
14
+ throw "jukebox.Manager requires jukebox.Player (Player.js) to run properly."
15
+ }
16
+
17
+
18
+ /*
19
+ * This is the transparent jukebox.Manager that runs in the background.
20
+ * You shouldn't have to call this constructor, only if you want to overwrite the
21
+ * defaults for having an own gameloop.
22
+ *
23
+ * The first parameter @settings {Map} overwrites the {#defaults}.
24
+ */
25
+ jukebox.Manager = function(settings) {
26
+
27
+ this.features = {};
28
+ this.codecs = {};
29
+
30
+ // Correction, Reset & Pause
31
+ this.__players = {};
32
+ this.__playersLength = 0;
33
+
34
+ // Queuing functionality
35
+ this.__clones = {};
36
+ this.__queue = [];
37
+
38
+
39
+ this.settings = {};
40
+
41
+ for (var d in this.defaults) {
42
+ this.settings[d] = this.defaults[d];
43
+ }
44
+
45
+ if (Object.prototype.toString.call(settings) === '[object Object]') {
46
+ for (var s in settings) {
47
+ this.settings[s] = settings[s];
48
+ }
49
+ }
50
+
51
+
52
+ this.__detectFeatures();
53
+
54
+
55
+ // If you don't want to use an own game loop
56
+ if (this.settings.useGameLoop === false) {
57
+
58
+ jukebox.Manager.__initialized = window.setInterval(function() {
59
+ jukebox.Manager.loop();
60
+ }, 20);
61
+
62
+ } else {
63
+ jukebox.Manager.__initialized = true;
64
+ }
65
+
66
+ };
67
+
68
+ jukebox.Manager.prototype = {
69
+
70
+ /*
71
+ * The defaults {Map} consist of two different flags.
72
+ *
73
+ * The @useFlash {Boolean} which is available for enforcing flash
74
+ * usage and the @useGameLoop {Boolean} that allows you to use your
75
+ * own game loop for avoiding multiple intervals inside the Browser.
76
+ *
77
+ * If @useGameLoop is set to {True} you will have to call the
78
+ * {#jukebox.Manager.loop} method everytime in your gameloop.
79
+ */
80
+ defaults: {
81
+ useFlash: false,
82
+ useGameLoop: false
83
+ },
84
+
85
+ __detectFeatures: function() {
86
+
87
+ /*
88
+ * HTML5 Audio Support
89
+ */
90
+ var audio = window.Audio && new Audio();
91
+
92
+ if (audio && audio.canPlayType && this.settings.useFlash === false) {
93
+
94
+ // Codec Detection MIME List
95
+ var mimeList = [
96
+ // e = extension, m = mime type
97
+ { e: '3gp', m: [ 'audio/3gpp', 'audio/amr' ] },
98
+ // { e: 'avi', m: 'video/x-msvideo' }, // avi container allows pretty everything, impossible to detect -.-
99
+ { e: 'aac', m: [ 'audio/aac', 'audio/aacp' ] },
100
+ { e: 'amr', m: [ 'audio/amr', 'audio/3gpp' ] },
101
+ // iOS aiff container that uses IMA4 (4:1 compression) on diff
102
+ { e: 'caf', m: [ 'audio/IMA-ADPCM', 'audio/x-adpcm', 'audio/x-aiff; codecs="IMA-ADPCM, ADPCM"' ] },
103
+ { e: 'm4a', m: [ 'audio/mp4', 'audio/mp4; codecs="mp4a.40.2,avc1.42E01E"', 'audio/mpeg4', 'audio/mpeg4-generic', 'audio/mp4a-latm', 'audio/MP4A-LATM', 'audio/x-m4a' ] },
104
+ { e: 'mp3', m: [ 'audio/mp3', 'audio/mpeg', 'audio/mpeg; codecs="mp3"', 'audio/MPA', 'audio/mpa-robust' ] }, // mpeg was name for mp2 and mp3! avi container was mp4/m4a
105
+ { e: 'mpga', m: [ 'audio/MPA', 'audio/mpa-robust', 'audio/mpeg', 'video/mpeg' ] },
106
+ { e: 'mp4', m: [ 'audio/mp4', 'video/mp4' ] },
107
+ { e: 'ogg', m: [ 'application/ogg', 'audio/ogg', 'audio/ogg; codecs="theora, vorbis"', 'video/ogg', 'video/ogg; codecs="theora, vorbis"' ] },
108
+ { e: 'wav', m: [ 'audio/wave', 'audio/wav', 'audio/wav; codecs="1"', 'audio/x-wav', 'audio/x-pn-wav' ] },
109
+ { e: 'webm', m: [ 'audio/webm', 'audio/webm; codecs="vorbis"', 'video/webm' ] }
110
+ ];
111
+
112
+ var mime, extension;
113
+ for (var m = 0, l = mimeList.length; m < l; m++) {
114
+
115
+ extension = mimeList[m].e;
116
+
117
+ if (mimeList[m].m.length && typeof mimeList[m].m === 'object') {
118
+
119
+ for (var mm = 0, mml = mimeList[m].m.length; mm < mml; mm++) {
120
+
121
+ mime = mimeList[m].m[mm];
122
+
123
+ // Supported Codec was found for Extension, so skip redundant checks
124
+ if (audio.canPlayType(mime) !== "") {
125
+ this.codecs[extension] = mime;
126
+ break;
127
+
128
+ // Flag the unsupported extension (that it is also not supported for Flash Fallback)
129
+ } else if (!this.codecs[extension]) {
130
+ this.codecs[extension] = false;
131
+ }
132
+
133
+ }
134
+
135
+ }
136
+
137
+ // Go, GC, Go for it!
138
+ mime = null;
139
+ extension = null;
140
+
141
+ }
142
+
143
+ // Browser supports HTML5 Audio API theoretically, but support depends on Codec Implementations
144
+ this.features.html5audio = !!(this.codecs.mp3 || this.codecs.ogg || this.codecs.webm || this.codecs.wav);
145
+
146
+ // Default Channel Amount is 8, known to work with all Browsers
147
+ this.features.channels = 8;
148
+
149
+ // Detect Volume support
150
+ audio.volume = 0.1337;
151
+ this.features.volume = !!(Math.abs(audio.volume - 0.1337) < 0.0001);
152
+
153
+
154
+ // FIXME: HACK, but there's no way to detect these crappy implementations
155
+ if (
156
+ // navigator.userAgent.match(/MSIE 9\.0/) ||
157
+ navigator.userAgent.match(/iPhone|iPod|iPad/i)) {
158
+ this.features.channels = 1;
159
+ }
160
+
161
+ }
162
+
163
+
164
+
165
+ /*
166
+ * Flash Audio Support
167
+ * Hint: All Android devices support Flash, even Android 1.6 ones
168
+ */
169
+ this.features.flashaudio = !!navigator.mimeTypes['application/x-shockwave-flash'] || !!navigator.plugins['Shockwave Flash'] || false;
170
+
171
+ // Internet Explorer
172
+ if (window.ActiveXObject){
173
+ try {
174
+ var flash = new ActiveXObject('ShockwaveFlash.ShockwaveFlash.10');
175
+ this.features.flashaudio = true;
176
+ } catch(e) {
177
+ // Throws an error if the version isn't available
178
+ }
179
+ }
180
+
181
+ // Allow enforce of Flash Usage
182
+ if (this.settings.useFlash === true) {
183
+ this.features.flashaudio = true;
184
+ }
185
+
186
+ if (this.features.flashaudio === true) {
187
+
188
+ // Overwrite Codecs only if there's no HTML5 Audio support
189
+ if (!this.features.html5audio) {
190
+
191
+ // Known to work with every Flash Implementation
192
+ this.codecs.mp3 = 'audio/mp3';
193
+ this.codecs.mpga = 'audio/mpeg';
194
+ this.codecs.mp4 = 'audio/mp4';
195
+ this.codecs.m4a = 'audio/mp4';
196
+
197
+
198
+ // Flash Runtime on Android also supports GSM codecs, but impossible to detect
199
+ this.codecs['3gp'] = 'audio/3gpp';
200
+ this.codecs.amr = 'audio/amr';
201
+
202
+
203
+ // TODO: Multi-Channel support on ActionScript-side
204
+ this.features.volume = true;
205
+ this.features.channels = 1;
206
+
207
+ }
208
+
209
+ }
210
+
211
+ },
212
+
213
+
214
+ __getPlayerById: function(id) {
215
+
216
+ if (this.__players && this.__players[id] !== undefined) {
217
+ return this.__players[id];
218
+ }
219
+
220
+ return null;
221
+
222
+ },
223
+
224
+ __getClone: function(origin, settings) {
225
+
226
+ // Search for a free clone
227
+ for (var cloneId in this.__clones) {
228
+
229
+ var clone = this.__clones[cloneId];
230
+ if (
231
+ clone.isPlaying === null
232
+ && clone.origin === origin
233
+ ) {
234
+ return clone;
235
+ }
236
+
237
+ }
238
+
239
+
240
+ // Create a new clone
241
+ if (Object.prototype.toString.call(settings) === '[object Object]') {
242
+
243
+ var cloneSettings = {};
244
+ for (var s in settings) {
245
+ cloneSettings[s] = settings[s];
246
+ }
247
+
248
+ // Clones just don't autoplay. Just don't :)
249
+ cloneSettings.autoplay = false;
250
+
251
+ var newClone = new jukebox.Player(cloneSettings, origin);
252
+ newClone.isClone = true;
253
+ newClone.wasReady = false;
254
+
255
+ this.__clones[newClone.id] = newClone;
256
+
257
+ return newClone;
258
+
259
+ }
260
+
261
+ return null;
262
+
263
+ },
264
+
265
+
266
+
267
+ /*
268
+ * PUBLIC API
269
+ */
270
+
271
+ /*
272
+ * This method is the stream-correction sound loop.
273
+ *
274
+ * In case you have overwritten the {jukebox.Manager} instance
275
+ * by yourself (with calling the constructor) and in case you
276
+ * have set the #settings.useGameLoop to {True}, you will have to
277
+ * call this method every time inside your gameloop.
278
+ */
279
+ loop: function() {
280
+
281
+ // Nothing to do
282
+ if (
283
+ this.__playersLength === 0
284
+ // || jukebox.Manager.__initialized !== true
285
+ ) {
286
+ return;
287
+ }
288
+
289
+
290
+ // Queue Functionality for Clone-supporting environments
291
+ if (
292
+ this.__queue.length
293
+ && this.__playersLength < this.features.channels
294
+ ) {
295
+
296
+ var queueEntry = this.__queue[0],
297
+ originPlayer = this.__getPlayerById(queueEntry.origin);
298
+
299
+ if (originPlayer !== null) {
300
+
301
+ var freeClone = this.__getClone(queueEntry.origin, originPlayer.settings);
302
+
303
+ // Use free clone for playback
304
+ if (freeClone !== null) {
305
+
306
+ if (this.features.volume === true) {
307
+ var originPlayer = this.__players[queueEntry.origin];
308
+ originPlayer && freeClone.setVolume(originPlayer.getVolume());
309
+ }
310
+
311
+ this.add(freeClone);
312
+ freeClone.play(queueEntry.pointer, true);
313
+
314
+ }
315
+
316
+ }
317
+
318
+ // Remove Queue Entry. It's corrupt if nothing happened.
319
+ this.__queue.splice(0, 1);
320
+
321
+ return;
322
+
323
+
324
+ // Queue Functionality for Single-Mode (iOS)
325
+ } else if (
326
+ this.__queue.length
327
+ && this.features.channels === 1
328
+ ) {
329
+
330
+ var queueEntry = this.__queue[0],
331
+ originPlayer = this.__getPlayerById(queueEntry.origin);
332
+
333
+ if (originPlayer !== null) {
334
+ originPlayer.play(queueEntry.pointer, true);
335
+ }
336
+
337
+ // Remove Queue Entry. It's corrupt if nothing happened
338
+ this.__queue.splice(0, 1);
339
+
340
+ }
341
+
342
+
343
+
344
+ for (var id in this.__players) {
345
+
346
+ var player = this.__players[id],
347
+ playerPosition = player.getCurrentTime() || 0;
348
+
349
+
350
+ // Correction
351
+ if (player.isPlaying && player.wasReady === false) {
352
+
353
+ player.wasReady = player.setCurrentTime(player.isPlaying.start);
354
+
355
+ // Reset / Stop
356
+ } else if (player.isPlaying && player.wasReady === true){
357
+
358
+ if (playerPosition > player.isPlaying.end) {
359
+
360
+ if (player.isPlaying.loop === true) {
361
+ player.play(player.isPlaying.start, true);
362
+ } else {
363
+ player.stop();
364
+ }
365
+
366
+ }
367
+
368
+
369
+ // Remove Idling Clones
370
+ } else if (player.isClone && player.isPlaying === null) {
371
+
372
+ this.remove(player);
373
+ continue;
374
+
375
+
376
+ // Background Music for Single-Mode (iOS)
377
+ } else if (player.backgroundMusic !== undefined && player.isPlaying === null) {
378
+
379
+ if (playerPosition > player.backgroundMusic.end) {
380
+ player.backgroundHackForiOS();
381
+ }
382
+
383
+ }
384
+
385
+ }
386
+
387
+
388
+ },
389
+
390
+ /*
391
+ * {String|Null} This method will check a @resources {Array} for playable resources
392
+ * due to codec and feature detection.
393
+ *
394
+ * It will return a {String} containing the preferred resource and {Null} if no
395
+ * playable resources was found.
396
+ *
397
+ * Hint: The highest preferred is the 0-index in the @resources {Array}. The latest
398
+ * index is the one with lowest preference.
399
+ */
400
+ getPlayableResource: function(resources) {
401
+
402
+ if (Object.prototype.toString.call(resources) !== '[object Array]') {
403
+ resources = [ resources ];
404
+ }
405
+
406
+
407
+ for (var r = 0, l = resources.length; r < l; r++) {
408
+
409
+ var resource = resources[r],
410
+ extension = resource.match(/\.([^\.]*)$/)[1];
411
+
412
+ // Yay! We found a supported resource!
413
+ if (extension && !!this.codecs[extension]) {
414
+ return resource;
415
+ }
416
+
417
+ }
418
+
419
+ return null;
420
+
421
+ },
422
+
423
+ /*
424
+ * {Boolean} This method will add a @player {jukebox.Player} instance to the stream-correction
425
+ * sound loop.
426
+ *
427
+ * It will return {True} if the {jukebox.Player} instance was successfully added
428
+ * and {False} if the @player was an invalid parameter.
429
+ */
430
+ add: function(player) {
431
+
432
+ if (
433
+ player instanceof jukebox.Player
434
+ && this.__players[player.id] === undefined
435
+ ) {
436
+ this.__playersLength++;
437
+ this.__players[player.id] = player;
438
+ return true;
439
+ }
440
+
441
+ return false;
442
+
443
+ },
444
+
445
+ /*
446
+ * {Boolean} This method will remove a @player {jukebox.Player} instance from
447
+ * the stream-correction sound loop.
448
+ *
449
+ * It will return {True} if the {jukebox.Player} instance was successfully removed
450
+ * and {False} if the @player was an invalid parameter.
451
+ */
452
+ remove: function(player) {
453
+
454
+ if (
455
+ player instanceof jukebox.Player
456
+ && this.__players[player.id] !== undefined
457
+ ) {
458
+ this.__playersLength--;
459
+ delete this.__players[player.id];
460
+ return true;
461
+ }
462
+
463
+ return false;
464
+
465
+ },
466
+
467
+ /*
468
+ * This method is kindof public, but only used internally
469
+ *
470
+ * DON'T USE IT!
471
+ */
472
+ addToQueue: function(pointer, playerId) {
473
+
474
+ if (
475
+ (typeof pointer === 'string' || typeof pointer === 'number')
476
+ && this.__players[playerId] !== undefined
477
+ ) {
478
+
479
+ this.__queue.push({
480
+ pointer: pointer,
481
+ origin: playerId
482
+ });
483
+
484
+ return true;
485
+
486
+ }
487
+
488
+ return false;
489
+
490
+ }
491
+
492
+ };
493
+
@@ -0,0 +1,660 @@
1
+ /*
2
+ * Jukebox
3
+ * http://github.com/zynga/jukebox
4
+ *
5
+ * Copyright 2011, Zynga Inc.
6
+ * Developed by Christoph Martens (@martensms)
7
+ *
8
+ * Licensed under the MIT License.
9
+ * https://raw.github.com/zynga/jukebox/master/MIT-LICENSE.txt
10
+ *
11
+ */
12
+
13
+ this.jukebox = {};
14
+
15
+ /*
16
+ * The first parameter @settings {Map} defines the settings of
17
+ * the created instance which overwrites the {#defaults}.
18
+ *
19
+ * The second optional parameter @origin {Number} is a unique id of
20
+ * another {jukebox.Player} instance, but it is only used internally
21
+ * by the {jukebox.Manager} for creating and managing clones.
22
+ */
23
+ jukebox.Player = function(settings, origin) {
24
+
25
+ this.id = ++jukebox.__jukeboxId;
26
+ this.origin = origin || null;
27
+
28
+
29
+ this.settings = {};
30
+
31
+ for (var d in this.defaults) {
32
+ this.settings[d] = this.defaults[d];
33
+ }
34
+
35
+ if (Object.prototype.toString.call(settings) === '[object Object]') {
36
+ for (var s in settings) {
37
+ this.settings[s] = settings[s];
38
+ }
39
+ }
40
+
41
+
42
+ /**
43
+ * #break(jukebox.Manager)
44
+ */
45
+
46
+ // Pseudo-Singleton to prevent double-initializaion
47
+ if (Object.prototype.toString.call(jukebox.Manager) === '[object Function]') {
48
+ jukebox.Manager = new jukebox.Manager();
49
+ }
50
+
51
+
52
+ this.isPlaying = null;
53
+ this.resource = null;
54
+
55
+
56
+ // Get playable resources via Feature / Codec Detection
57
+ if (Object.prototype.toString.call(jukebox.Manager) === '[object Object]') {
58
+ this.resource = jukebox.Manager.getPlayableResource(this.settings.resources);
59
+ } else {
60
+ this.resource = this.settings.resources[0] || null;
61
+ }
62
+
63
+
64
+ if (this.resource === null) {
65
+ throw "Your browser can't playback the given resources - or you have missed to include jukebox.Manager";
66
+ } else {
67
+ this.__init();
68
+ }
69
+
70
+
71
+ return this;
72
+
73
+ };
74
+
75
+ jukebox.__jukeboxId = 0;
76
+
77
+ jukebox.Player.prototype = {
78
+
79
+ /*
80
+ * The defaults which are overwritten by the {#constructor}'s
81
+ * settings parameter.
82
+ *
83
+ * @resources contains an {Array} of File URL {String}s
84
+ * @spritemap is a Hashmap containing multiple @sprite-entry {Object}
85
+ *
86
+ * @autoplay is an optional {String} that autoplays a @sprite-entry
87
+ *
88
+ * @flashMediaElement is an optional setting that contains the
89
+ * relative URL {String} to the FlashMediaElement.swf for flash fallback.
90
+ *
91
+ * @timeout is a {Number} in milliseconds that is used if no "canplaythrough"
92
+ * event is fired on the Audio Node.
93
+ */
94
+ defaults: {
95
+ resources: [],
96
+ autoplay: false,
97
+ spritemap: {},
98
+ flashMediaElement: '<%= asset_path 'FlashMediaElement.swf' %>',
99
+ timeout: 1000
100
+ },
101
+
102
+
103
+ /*
104
+ * PRIVATE API
105
+ */
106
+ __addToManager: function(event) {
107
+
108
+ if (this.__wasAddedToManager !== true) {
109
+ jukebox.Manager.add(this);
110
+ this.__wasAddedToManager = true;
111
+ }
112
+
113
+ },
114
+
115
+ /*
116
+ __log: function(title, desc) {
117
+
118
+ if (!this.__logElement) {
119
+ this.__logElement = document.createElement('ul');
120
+ document.body.appendChild(this.__logElement);
121
+ }
122
+
123
+ var that = this;
124
+ window.setTimeout(function() {
125
+ var item = document.createElement('li');
126
+ item.innerHTML = '<b>' + title + '</b>: ' + (desc ? desc : '');
127
+ that.__logElement.appendChild(item);
128
+ }, 0);
129
+
130
+ },
131
+
132
+ __updateBuffered: function(event) {
133
+
134
+ var buffer = this.context.buffered;
135
+
136
+ if (buffer) {
137
+
138
+ for (var b = 0; b < buffer.length; b++) {
139
+ this.__log(event.type, buffer.start(b).toString() + ' / ' + buffer.end(b).toString());
140
+ }
141
+
142
+ }
143
+
144
+ },
145
+ */
146
+
147
+
148
+ __init: function() {
149
+
150
+ var that = this,
151
+ settings = this.settings,
152
+ features = {},
153
+ api;
154
+
155
+ if (jukebox.Manager && jukebox.Manager.features !== undefined) {
156
+ features = jukebox.Manager.features;
157
+ }
158
+
159
+ // HTML5 Audio
160
+ if (features.html5audio === true) {
161
+
162
+ this.context = new Audio();
163
+ this.context.src = this.resource;
164
+
165
+ if (this.origin === null) {
166
+
167
+ // This will add the stream to the manager's stream cache,
168
+ // there's a fallback timeout if the canplaythrough event wasn't fired
169
+ var addFunc = function(event){ that.__addToManager(event); };
170
+ this.context.addEventListener('canplaythrough', addFunc, true);
171
+
172
+ // Uh, Oh, What is it good for? Uh, Oh ...
173
+ /*
174
+ var bufferFunc = function(event) { that.__updateBuffered(event); };
175
+ this.context.addEventListener('loadedmetadata', bufferFunc, true);
176
+ this.context.addEventListener('progress', bufferFunc, true);
177
+ */
178
+
179
+ // This is the timeout, we will penetrate the currentTime anyways.
180
+ window.setTimeout(function(){
181
+ that.context.removeEventListener('canplaythrough', addFunc, true);
182
+ addFunc('timeout');
183
+ }, settings.timeout);
184
+
185
+ }
186
+
187
+ // old WebKit
188
+ this.context.autobuffer = true;
189
+
190
+ // new WebKit
191
+ this.context.preload = true;
192
+
193
+
194
+ // FIXME: This is the hacky API, but got no more generic idea for now =/
195
+ for (api in this.HTML5API) {
196
+ this[api] = this.HTML5API[api];
197
+ }
198
+
199
+ if (features.channels > 1) {
200
+
201
+ if (settings.autoplay === true) {
202
+ this.context.autoplay = true;
203
+ } else if (settings.spritemap[settings.autoplay] !== undefined) {
204
+ this.play(settings.autoplay);
205
+ }
206
+
207
+ } else if (features.channels === 1 && settings.spritemap[settings.autoplay] !== undefined) {
208
+
209
+ this.backgroundMusic = settings.spritemap[settings.autoplay];
210
+ this.backgroundMusic.started = Date.now ? Date.now() : +new Date();
211
+
212
+ // Initial playback will do the trick for iOS' security model
213
+ this.play(settings.autoplay);
214
+
215
+ }
216
+
217
+ // Pause audio on screen timeout because it can't be controlled then.
218
+ if (features.channels == 1 && settings.canPlayBackground !== true) {
219
+ // This does not work in iOS < 5.0 and Windows Phone.
220
+ // Calling audio.pause() after onbeforeunload event on Windows Phone may
221
+ // remove all audio from the browser until you restart the device.
222
+ window.addEventListener('pagehide', function() {
223
+ if (that.isPlaying !== null) {
224
+ that.pause();
225
+ that.__wasAutoPaused = true;
226
+ }
227
+ });
228
+ window.addEventListener('pageshow', function() {
229
+ if (that.__wasAutoPaused) {
230
+ that.resume();
231
+ delete that._wasAutoPaused;
232
+ }
233
+ });
234
+ }
235
+
236
+
237
+ // Flash Audio
238
+ } else if (features.flashaudio === true) {
239
+
240
+ // FIXME: This is the hacky API, but got no more generic idea for now =/
241
+ for (api in this.FLASHAPI) {
242
+ this[api] = this.FLASHAPI[api];
243
+ }
244
+
245
+ var flashVars = [
246
+ 'id=jukebox-flashstream-' + this.id,
247
+ 'autoplay=' + settings.autoplay,
248
+ 'file=' + window.encodeURIComponent(this.resource)
249
+ ];
250
+
251
+ // Too much crappy code, have this in a crappy function instead.
252
+ this.__initFlashContext(flashVars);
253
+
254
+ if (settings.autoplay === true) {
255
+ this.play(0);
256
+ } else if (settings.spritemap[settings.autoplay]) {
257
+ this.play(settings.autoplay);
258
+ }
259
+
260
+ } else {
261
+
262
+ throw "Your Browser does not support Flash Audio or HTML5 Audio.";
263
+
264
+ }
265
+
266
+ },
267
+
268
+ /*
269
+ * This is not that simple, better code structure with a helper function
270
+ */
271
+ __initFlashContext: function(flashVars) {
272
+
273
+ var context,
274
+ url = this.settings.flashMediaElement,
275
+ p;
276
+
277
+ var params = {
278
+ 'flashvars': flashVars.join('&'),
279
+ 'quality': 'high',
280
+ 'bgcolor': '#000000',
281
+ 'wmode': 'transparent',
282
+ 'allowscriptaccess': 'always',
283
+ 'allowfullscreen': 'true'
284
+ };
285
+
286
+ /*
287
+ * IE will only render a Shockwave Flash file if there's this crappy outerHTML used.
288
+ */
289
+ if (navigator.userAgent.match(/MSIE/)) {
290
+
291
+ context = document.createElement('div');
292
+
293
+ // outerHTML only works in IE when context is already in DOM
294
+ document.getElementsByTagName('body')[0].appendChild(context);
295
+
296
+
297
+ var object = document.createElement('object');
298
+
299
+ object.id = 'jukebox-flashstream-' + this.id;
300
+ object.setAttribute('type', 'application/x-shockwave-flash');
301
+ object.setAttribute('classid', 'clsid:d27cdb6e-ae6d-11cf-96b8-444553540000');
302
+ object.setAttribute('width', '0');
303
+ object.setAttribute('height', '0');
304
+
305
+
306
+ // IE specific params
307
+ params.movie = url + '?x=' + (Date.now ? Date.now() : +new Date());
308
+ params.flashvars = flashVars.join('&amp;');
309
+
310
+
311
+
312
+ for (p in params) {
313
+
314
+ var element = document.createElement('param');
315
+ element.setAttribute('name', p);
316
+ element.setAttribute('value', params[p]);
317
+ object.appendChild(element);
318
+
319
+ }
320
+
321
+ context.outerHTML = object.outerHTML;
322
+
323
+ this.context = document.getElementById('jukebox-flashstream-' + this.id);
324
+
325
+
326
+ /*
327
+ * This is the case for a cool, but outdated Browser
328
+ * ... like Netscape or so ;)
329
+ */
330
+ } else {
331
+
332
+ context = document.createElement('embed');
333
+ context.id = 'jukebox-flashstream-' + this.id;
334
+ context.setAttribute('type', 'application/x-shockwave-flash');
335
+ context.setAttribute('width', '100');
336
+ context.setAttribute('height', '100');
337
+
338
+ params.play = false;
339
+ params.loop = false;
340
+ params.src = url + '?x=' + (Date.now ? Date.now() : +new Date());
341
+
342
+ for (p in params) {
343
+ context.setAttribute(p, params[p]);
344
+ }
345
+
346
+ document.getElementsByTagName('body')[0].appendChild(context);
347
+
348
+ this.context = context;
349
+
350
+ }
351
+
352
+ },
353
+
354
+ /*
355
+ * This is the background hack for iOS and other single-channel systems
356
+ * It allows playback of a background music, which will be overwritten by playbacks
357
+ * of other sprite entries. After these entries, background music continues.
358
+ *
359
+ * This allows us to trick out the iOS Security Model after initial playback =)
360
+ */
361
+ backgroundHackForiOS: function() {
362
+
363
+ if (this.backgroundMusic === undefined) {
364
+ return;
365
+ }
366
+
367
+ var now = Date.now ? Date.now() : +new Date();
368
+
369
+ if (this.backgroundMusic.started === undefined) {
370
+
371
+ this.backgroundMusic.started = now;
372
+ this.setCurrentTime(this.backgroundMusic.start);
373
+
374
+ } else {
375
+
376
+ this.backgroundMusic.lastPointer = (( now - this.backgroundMusic.started) / 1000) % (this.backgroundMusic.end - this.backgroundMusic.start) + this.backgroundMusic.start;
377
+ this.play(this.backgroundMusic.lastPointer);
378
+
379
+ }
380
+
381
+ },
382
+
383
+
384
+
385
+ /*
386
+ * PUBLIC API
387
+ */
388
+
389
+ /*
390
+ * This method will try to playback a given @pointer position of the stream.
391
+ * The @pointer position can be either a {String} of a sprite entry inside
392
+ * {#settings.spritemap} or a {Number} in seconds.
393
+ *
394
+ * The optional parameter @enforce is a {Boolean} that enforces the stream
395
+ * playback and avoids queueing or work delegation to a free clone.
396
+ */
397
+ play: function(pointer, enforce) {
398
+
399
+ if (this.isPlaying !== null && enforce !== true) {
400
+
401
+ if (jukebox.Manager !== undefined) {
402
+ jukebox.Manager.addToQueue(pointer, this.id);
403
+ }
404
+
405
+ return;
406
+
407
+ }
408
+
409
+ var spritemap = this.settings.spritemap,
410
+ newPosition;
411
+
412
+ // Spritemap Entry Playback
413
+ if (spritemap[pointer] !== undefined) {
414
+
415
+ newPosition = spritemap[pointer].start;
416
+
417
+ // Seconds-Position Playback (find out matching spritemap entry)
418
+ } else if (typeof pointer === 'number') {
419
+
420
+ newPosition = pointer;
421
+
422
+ for (var s in spritemap) {
423
+
424
+ if (newPosition >= spritemap[s].start && newPosition <= spritemap[s].end) {
425
+ pointer = s;
426
+ break;
427
+ }
428
+
429
+ }
430
+
431
+ }
432
+
433
+ if (newPosition !== undefined && Object.prototype.toString.call(spritemap[pointer]) === '[object Object]') {
434
+
435
+ this.isPlaying = this.settings.spritemap[pointer];
436
+
437
+ // Start Playback, stream position will be corrected by jukebox.Manager
438
+ if (this.context.play) {
439
+ this.context.play();
440
+ }
441
+
442
+ // Locking due to slow Implementation on Mobile Devices
443
+ this.wasReady = this.setCurrentTime(newPosition);
444
+
445
+ }
446
+
447
+ },
448
+
449
+ /*
450
+ * This method will stop the current playback and resets the pointer that is
451
+ * cached by {#pause} method calls.
452
+ *
453
+ * It automatically starts the backgroundMusic for single-stream environments.
454
+ */
455
+ stop: function() {
456
+
457
+ this.__lastPosition = 0; // reset pointer
458
+ this.isPlaying = null;
459
+
460
+ // Was a Background Music played already?
461
+ if (this.backgroundMusic) {
462
+ this.backgroundHackForiOS();
463
+ } else {
464
+ this.context.pause();
465
+ }
466
+
467
+ return true;
468
+
469
+ },
470
+
471
+ /*
472
+ * {Number} This method will pause the current playback and cache the current position
473
+ * that is used by {#resume} on its next call.
474
+ *
475
+ * It returns the last position {Number} in seconds, so that you can optionally
476
+ * use it in the {#resume} method call.
477
+ */
478
+ pause: function() {
479
+
480
+ this.isPlaying = null;
481
+
482
+ this.__lastPosition = this.getCurrentTime();
483
+ this.context.pause();
484
+
485
+ return this.__lastPosition;
486
+
487
+ },
488
+
489
+ /*
490
+ * {Boolean} This method will resume playback. If the optional parameter @position
491
+ * {Number} is not used, it will try to playback the last cached position from the
492
+ * last {#pause} method call.
493
+ *
494
+ * If no @position and no cached position is available, it will start playback - no
495
+ * matter where the stream is currently at.
496
+ *
497
+ * It returns {True} if a cached position was used. If no given and no cached
498
+ * position was used for playback, it will return {False}
499
+ */
500
+ resume: function(position) {
501
+
502
+ position = typeof position === 'number' ? position : this.__lastPosition;
503
+
504
+ if (position !== null) {
505
+
506
+ this.play(position);
507
+ this.__lastPosition = null;
508
+ return true;
509
+
510
+ } else {
511
+
512
+ this.context.play();
513
+ return false;
514
+
515
+ }
516
+
517
+ },
518
+
519
+
520
+
521
+ /*
522
+ * HTML5 Audio API abstraction layer
523
+ */
524
+ HTML5API: {
525
+
526
+ /*
527
+ * {Number} This method will return the current volume as a {Number}
528
+ * from 0 to 1.0.
529
+ */
530
+ getVolume: function() {
531
+ return this.context.volume || 1;
532
+ },
533
+
534
+ /*
535
+ * This method will set the volume to a given @value that is a {Number}
536
+ * from 0 to 1.0.
537
+ */
538
+ setVolume: function(value) {
539
+
540
+ this.context.volume = value;
541
+
542
+ // This is apparently only for mobile implementations
543
+ if (Math.abs(this.context.volume - value) < 0.0001) {
544
+ return true;
545
+ }
546
+
547
+
548
+ return false;
549
+
550
+ },
551
+
552
+ /*
553
+ * {Number} This method will return the current pointer position in
554
+ * the stream in seconds.
555
+ */
556
+ getCurrentTime: function() {
557
+ return this.context.currentTime || 0;
558
+ },
559
+
560
+ /*
561
+ * {Boolean} This method will set the current pointer position to a
562
+ * new @value {Number} in seconds.
563
+ *
564
+ * It returns {True} on success, {False} if the stream wasn't ready
565
+ * at the given stream position @value.
566
+ */
567
+ setCurrentTime: function(value) {
568
+
569
+ try {
570
+ // DOM Exceptions are fired when Audio Element isn't ready yet.
571
+ this.context.currentTime = value;
572
+ return true;
573
+ } catch(e) {
574
+ return false;
575
+ }
576
+
577
+ }
578
+
579
+ },
580
+
581
+
582
+
583
+ /*
584
+ * Flash Audio API abstraction layer
585
+ */
586
+ FLASHAPI: {
587
+
588
+ /*
589
+ * {Number} This method will return the current volume of the stream as
590
+ * a {Number} from 0 to 1.0, considering the Flash JavaScript API is
591
+ * ready for access.
592
+ */
593
+ getVolume: function() {
594
+
595
+ // Avoid stupid exceptions, wait for JavaScript API to be ready
596
+ if (this.context && typeof this.context.getVolume === 'function') {
597
+ return this.context.getVolume();
598
+ }
599
+
600
+ return 1;
601
+
602
+ },
603
+
604
+ /*
605
+ * {Boolean} This method will set the volume to a given @value which is
606
+ * a {Number} from 0 to 1.0. It will return {True} if the Flash
607
+ * JavaScript API is ready for access. It returns {False} if the Flash
608
+ * JavaScript API wasn't ready.
609
+ */
610
+ setVolume: function(value) {
611
+
612
+ // Avoid stupid exceptions, wait for JavaScript API to be ready
613
+ if (this.context && typeof this.context.setVolume === 'function') {
614
+ this.context.setVolume(value);
615
+ return true;
616
+ }
617
+
618
+ return false;
619
+
620
+ },
621
+
622
+ /*
623
+ * {Number} This method will return the pointer position in the stream in
624
+ * seconds.
625
+ *
626
+ * If the Flash JavaScript API wasn't ready, the pointer position is 0.
627
+ */
628
+ getCurrentTime: function() {
629
+
630
+ // Avoid stupid exceptions, wait for JavaScript API to be ready
631
+ if (this.context && typeof this.context.getCurrentTime === 'function') {
632
+ return this.context.getCurrentTime();
633
+ }
634
+
635
+ return 0;
636
+
637
+ },
638
+
639
+ /*
640
+ * {Boolean} This method will set the pointer position to a given @value {Number}
641
+ * in seconds.
642
+ *
643
+ * It will return {True} if the Flash JavaScript API was ready. If not, it
644
+ * will return {False}.
645
+ */
646
+ setCurrentTime: function(value) {
647
+
648
+ // Avoid stupid exceptions, wait for JavaScript API to be ready
649
+ if (this.context && typeof this.context.setCurrentTime === 'function') {
650
+ return this.context.setCurrentTime(value);
651
+ }
652
+
653
+ return false;
654
+
655
+ }
656
+
657
+ }
658
+
659
+ };
660
+
@@ -0,0 +1,2 @@
1
+ // = require Player
2
+ // = require Manager
metadata ADDED
@@ -0,0 +1,59 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jukebox-rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Alif Rachmawadi
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-09-19 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: ! 'Zynga''s Jukebox: Sophisticated audio playback for the web.'
15
+ email:
16
+ - subosito@gmail.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - .gitignore
22
+ - Gemfile
23
+ - LICENSE
24
+ - README.md
25
+ - Rakefile
26
+ - jukebox-rails.gemspec
27
+ - lib/jukebox-rails.rb
28
+ - lib/jukebox-rails/version.rb
29
+ - vendor/assets/javascripts/FlashMediaElement.swf
30
+ - vendor/assets/javascripts/Manager.js
31
+ - vendor/assets/javascripts/Player.js.erb
32
+ - vendor/assets/javascripts/jukebox.js
33
+ homepage: https://github.com/subosito/jukebox-rails
34
+ licenses: []
35
+ post_install_message:
36
+ rdoc_options: []
37
+ require_paths:
38
+ - lib
39
+ required_ruby_version: !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ! '>='
43
+ - !ruby/object:Gem::Version
44
+ version: '0'
45
+ required_rubygems_version: !ruby/object:Gem::Requirement
46
+ none: false
47
+ requirements:
48
+ - - ! '>='
49
+ - !ruby/object:Gem::Version
50
+ version: '0'
51
+ requirements: []
52
+ rubyforge_project:
53
+ rubygems_version: 1.8.23
54
+ signing_key:
55
+ specification_version: 3
56
+ summary: The Jukebox is a component for playing sounds and music with the usage of
57
+ sprites with a special focus on performance and cross-device deployment.
58
+ test_files: []
59
+ has_rdoc: