jukebox-rails 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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: