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.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +29 -0
- data/Rakefile +2 -0
- data/jukebox-rails.gemspec +17 -0
- data/lib/jukebox-rails.rb +8 -0
- data/lib/jukebox-rails/version.rb +5 -0
- data/vendor/assets/javascripts/FlashMediaElement.swf +0 -0
- data/vendor/assets/javascripts/Manager.js +493 -0
- data/vendor/assets/javascripts/Player.js.erb +660 -0
- data/vendor/assets/javascripts/jukebox.js +2 -0
- metadata +59 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
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.
|
data/README.md
ADDED
|
@@ -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
|
data/Rakefile
ADDED
|
@@ -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
|
|
Binary file
|
|
@@ -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('&');
|
|
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
|
+
|
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:
|