soundcloud-custom-player-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.
Files changed (52) hide show
  1. data/.gitignore +20 -0
  2. data/Gemfile +4 -0
  3. data/LICENSE.txt +22 -0
  4. data/README.md +35 -0
  5. data/Rakefile +1 -0
  6. data/lib/soundcloud-custom-player-rails.rb +12 -0
  7. data/lib/soundcloud-custom-player-rails/engine.rb +6 -0
  8. data/lib/soundcloud-custom-player-rails/railtie.rb +6 -0
  9. data/lib/soundcloud-custom-player-rails/version.rb +3 -0
  10. data/soundcloud-custom-player-rails.gemspec +17 -0
  11. data/vendor/assets/images/sc-player-artwork/pause-hover.png +0 -0
  12. data/vendor/assets/images/sc-player-artwork/pause.png +0 -0
  13. data/vendor/assets/images/sc-player-artwork/play-hover.png +0 -0
  14. data/vendor/assets/images/sc-player-artwork/play.png +0 -0
  15. data/vendor/assets/images/sc-player-minimal/pause-hover.png +0 -0
  16. data/vendor/assets/images/sc-player-minimal/pause.png +0 -0
  17. data/vendor/assets/images/sc-player-minimal/play-hover.png +0 -0
  18. data/vendor/assets/images/sc-player-minimal/play.png +0 -0
  19. data/vendor/assets/images/sc-player-red/pause-hover.png +0 -0
  20. data/vendor/assets/images/sc-player-red/pause.png +0 -0
  21. data/vendor/assets/images/sc-player-red/play-hover.png +0 -0
  22. data/vendor/assets/images/sc-player-red/play.png +0 -0
  23. data/vendor/assets/images/sc-player-standard/pause-blue-hover.png +0 -0
  24. data/vendor/assets/images/sc-player-standard/pause-blue.png +0 -0
  25. data/vendor/assets/images/sc-player-standard/pause-green-hover.png +0 -0
  26. data/vendor/assets/images/sc-player-standard/pause-green.png +0 -0
  27. data/vendor/assets/images/sc-player-standard/pause-orange-hover.png +0 -0
  28. data/vendor/assets/images/sc-player-standard/pause-orange.png +0 -0
  29. data/vendor/assets/images/sc-player-standard/play-blue-hover.png +0 -0
  30. data/vendor/assets/images/sc-player-standard/play-blue.png +0 -0
  31. data/vendor/assets/images/sc-player-standard/play-green-hover.png +0 -0
  32. data/vendor/assets/images/sc-player-standard/play-green.png +0 -0
  33. data/vendor/assets/images/sc-player-standard/play-orange-hover.png +0 -0
  34. data/vendor/assets/images/sc-player-standard/play-orange.png +0 -0
  35. data/vendor/assets/javascripts/sc-player.js +725 -0
  36. data/vendor/assets/javascripts/soundcloud.player.api.js +140 -0
  37. data/vendor/assets/stylesheets/sc-player-artwork/colors.css +130 -0
  38. data/vendor/assets/stylesheets/sc-player-artwork/standards.css +18 -0
  39. data/vendor/assets/stylesheets/sc-player-artwork/structure.css +173 -0
  40. data/vendor/assets/stylesheets/sc-player-minimal/colors.css +138 -0
  41. data/vendor/assets/stylesheets/sc-player-minimal/standards.css +17 -0
  42. data/vendor/assets/stylesheets/sc-player-minimal/structure.css +174 -0
  43. data/vendor/assets/stylesheets/sc-player-red/colors.css +141 -0
  44. data/vendor/assets/stylesheets/sc-player-red/standards.css +32 -0
  45. data/vendor/assets/stylesheets/sc-player-red/structure.css +171 -0
  46. data/vendor/assets/stylesheets/sc-player-standard/colors-blue.css +299 -0
  47. data/vendor/assets/stylesheets/sc-player-standard/colors-green.css +167 -0
  48. data/vendor/assets/stylesheets/sc-player-standard/colors-orange.css +165 -0
  49. data/vendor/assets/stylesheets/sc-player-standard/standards.css +33 -0
  50. data/vendor/assets/stylesheets/sc-player-standard/structure-horizontal.css +209 -0
  51. data/vendor/assets/stylesheets/sc-player-standard/structure-vertical.css +190 -0
  52. metadata +96 -0
@@ -0,0 +1,20 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ coverage
6
+ InstalledFiles
7
+ lib/bundler/man
8
+ pkg
9
+ rdoc
10
+ spec/reports
11
+ test/tmp
12
+ test/version_tmp
13
+ tmp
14
+
15
+ # YARD artifacts
16
+ .yardoc
17
+ _yardoc
18
+ doc/
19
+ .DS_Store
20
+ Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in ..gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Daniël van de Burgt
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,35 @@
1
+ soundcloud-custom-player-rails
2
+ ==============================
3
+
4
+ Soundcloud Custom Player integration for Rails asset pipeline.
5
+ See https://github.com/soundcloud/soundcloud-custom-player for more info.
6
+
7
+ Installation
8
+ ------------
9
+
10
+ Gemfile:
11
+
12
+ group :assets do
13
+ gem 'soundcloud-custom-player-rails'
14
+ end
15
+
16
+ Or get the latest build:
17
+
18
+ group :assets do
19
+ gem 'soundcloud-custom-player-rails',
20
+ :require => 'soundcloud-custom-player-rails',
21
+ :git => 'git://github.com/thatdutchguy/soundcloud-custom-player-rails.git'
22
+ end
23
+
24
+ js:
25
+
26
+ //= require soundcloud.player.api
27
+ //= require sc-player
28
+
29
+ css:
30
+
31
+ @import "sc-player-standard/standards";
32
+ @import "sc-player-standard/structure-horizontal";
33
+ @import "sc-player-standard/colors-orange";
34
+
35
+ Included are multiple players/css combinations, see https://github.com/thatdutchguy/soundcloud-custom-player-rails/tree/master/vendor/assets/stylesheets too see what's available.
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,12 @@
1
+ require "rails"
2
+ require "soundcloud-custom-player-rails/version"
3
+
4
+ module SoundcloudCustomPlayerRails
5
+ module Rails
6
+ if ::Rails.version < "3.1"
7
+ require "soundcloud-custom-player-rails/railtie"
8
+ else
9
+ require "soundcloud-custom-player-rails/engine"
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,6 @@
1
+ module SoundcloudCustomPlayerRails
2
+ module Rails
3
+ class Engine < ::Rails::Engine
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module SoundcloudCustomPlayerRails
2
+ module Rails
3
+ class Railtie < ::Rails::Railtie
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,3 @@
1
+ module SoundcloudCustomPlayerRails
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,17 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'soundcloud-custom-player-rails/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "soundcloud-custom-player-rails"
8
+ gem.version = SoundcloudCustomPlayerRails::VERSION
9
+ gem.authors = ["Daniël van de Burgt"]
10
+ gem.email = ["thatdutchguy@secretlymexico.com"]
11
+ gem.description = %q{Soundcloud Custom Player assets for Rails}
12
+ gem.summary = gem.description
13
+ gem.homepage = "http://github.com/thatdutchguy/soundcloud-custom-player-rails"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.require_paths = ["lib"]
17
+ end
@@ -0,0 +1,725 @@
1
+ /*
2
+ * SoundCloud Custom Player jQuery Plugin
3
+ * Author: Matas Petrikas, matas@soundcloud.com
4
+ * Copyright (c) 2009 SoundCloud Ltd.
5
+ * Licensed under the MIT license:
6
+ * http://www.opensource.org/licenses/mit-license.php
7
+ *
8
+ * Usage:
9
+ * <a href="http://soundcloud.com/matas/hobnotropic" class="sc-player">My new dub track</a>
10
+ * The link will be automatically replaced by the HTML based player
11
+ */
12
+ (function($) {
13
+ // Convert milliseconds into Hours (h), Minutes (m), and Seconds (s)
14
+ var timecode = function(ms) {
15
+ var hms = function(ms) {
16
+ return {
17
+ h: Math.floor(ms/(60*60*1000)),
18
+ m: Math.floor((ms/60000) % 60),
19
+ s: Math.floor((ms/1000) % 60)
20
+ };
21
+ }(ms),
22
+ tc = []; // Timecode array to be joined with '.'
23
+
24
+ if (hms.h > 0) {
25
+ tc.push(hms.h);
26
+ }
27
+
28
+ tc.push((hms.m < 10 && hms.h > 0 ? "0" + hms.m : hms.m));
29
+ tc.push((hms.s < 10 ? "0" + hms.s : hms.s));
30
+
31
+ return tc.join('.');
32
+ };
33
+ // shuffle the array
34
+ var shuffle = function(arr) {
35
+ arr.sort(function() { return 1 - Math.floor(Math.random() * 3); } );
36
+ return arr;
37
+ };
38
+
39
+ var debug = true,
40
+ useSandBox = false,
41
+ $doc = $(document),
42
+ log = function(args) {
43
+ try {
44
+ if(debug && window.console && window.console.log){
45
+ window.console.log.apply(window.console, arguments);
46
+ }
47
+ } catch (e) {
48
+ // no console available
49
+ }
50
+ },
51
+ domain = useSandBox ? 'sandbox-soundcloud.com' : 'soundcloud.com',
52
+ secureDocument = (document.location.protocol === 'https:'),
53
+ // convert a SoundCloud resource URL to an API URL
54
+ scApiUrl = function(url, apiKey) {
55
+ var resolver = ( secureDocument || (/^https/i).test(url) ? 'https' : 'http') + '://api.' + domain + '/resolve?url=',
56
+ params = 'format=json&consumer_key=' + apiKey +'&callback=?';
57
+
58
+ // force the secure url in the secure environment
59
+ if( secureDocument ) {
60
+ url = url.replace(/^http:/, 'https:');
61
+ }
62
+
63
+ // check if it's already a resolved api url
64
+ if ( (/api\./).test(url) ) {
65
+ return url + '?' + params;
66
+ } else {
67
+ return resolver + url + '&' + params;
68
+ }
69
+ };
70
+
71
+ // TODO Expose the audio engine, so it can be unit-tested
72
+ var audioEngine = function() {
73
+ var html5AudioAvailable = function() {
74
+ var state = false;
75
+ try{
76
+ var a = new Audio();
77
+ state = a.canPlayType && (/maybe|probably/).test(a.canPlayType('audio/mpeg'));
78
+ // uncomment the following line, if you want to enable the html5 audio only on mobile devices
79
+ // state = state && (/iPad|iphone|mobile|pre\//i).test(navigator.userAgent);
80
+ }catch(e){
81
+ // there's no audio support here sadly
82
+ }
83
+
84
+ return state;
85
+ }(),
86
+ callbacks = {
87
+ onReady: function() {
88
+ $doc.trigger('scPlayer:onAudioReady');
89
+ },
90
+ onPlay: function() {
91
+ $doc.trigger('scPlayer:onMediaPlay');
92
+ },
93
+ onPause: function() {
94
+ $doc.trigger('scPlayer:onMediaPause');
95
+ },
96
+ onEnd: function() {
97
+ $doc.trigger('scPlayer:onMediaEnd');
98
+ },
99
+ onBuffer: function(percent) {
100
+ $doc.trigger({type: 'scPlayer:onMediaBuffering', percent: percent});
101
+ }
102
+ };
103
+
104
+ var html5Driver = function() {
105
+ var player = new Audio(),
106
+ onTimeUpdate = function(event){
107
+ var obj = event.target,
108
+ buffer = ((obj.buffered.length && obj.buffered.end(0)) / obj.duration) * 100;
109
+ // ipad has no progress events implemented yet
110
+ callbacks.onBuffer(buffer);
111
+ // anounce if it's finished for the clients without 'ended' events implementation
112
+ if (obj.currentTime === obj.duration) { callbacks.onEnd(); }
113
+ },
114
+ onProgress = function(event) {
115
+ var obj = event.target,
116
+ buffer = ((obj.buffered.length && obj.buffered.end(0)) / obj.duration) * 100;
117
+ callbacks.onBuffer(buffer);
118
+ };
119
+
120
+ $('<div class="sc-player-engine-container"></div>').appendTo(document.body).append(player);
121
+
122
+ // prepare the listeners
123
+ player.addEventListener('play', callbacks.onPlay, false);
124
+ player.addEventListener('pause', callbacks.onPause, false);
125
+ // handled in the onTimeUpdate for now untill all the browsers support 'ended' event
126
+ // player.addEventListener('ended', callbacks.onEnd, false);
127
+ player.addEventListener('timeupdate', onTimeUpdate, false);
128
+ player.addEventListener('progress', onProgress, false);
129
+
130
+
131
+ return {
132
+ load: function(track, apiKey) {
133
+ player.pause();
134
+ player.src = track.stream_url + (/\?/.test(track.stream_url) ? '&' : '?') + 'consumer_key=' + apiKey;
135
+ player.load();
136
+ player.play();
137
+ },
138
+ play: function() {
139
+ player.play();
140
+ },
141
+ pause: function() {
142
+ player.pause();
143
+ },
144
+ stop: function(){
145
+ if (player.currentTime) {
146
+ player.currentTime = 0;
147
+ player.pause();
148
+ }
149
+ },
150
+ seek: function(relative){
151
+ player.currentTime = player.duration * relative;
152
+ player.play();
153
+ },
154
+ getDuration: function() {
155
+ return player.duration * 1000;
156
+ },
157
+ getPosition: function() {
158
+ return player.currentTime * 1000;
159
+ },
160
+ setVolume: function(val) {
161
+ player.volume = val / 100;
162
+ }
163
+ };
164
+
165
+ };
166
+
167
+
168
+
169
+ var flashDriver = function() {
170
+ var engineId = 'scPlayerEngine',
171
+ player,
172
+ flashHtml = function(url) {
173
+ var swf = (secureDocument ? 'https' : 'http') + '://player.' + domain +'/player.swf?url=' + url +'&amp;enable_api=true&amp;player_type=engine&amp;object_id=' + engineId;
174
+ if ($.browser.msie) {
175
+ return '<object height="100%" width="100%" id="' + engineId + '" classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" data="' + swf + '">'+
176
+ '<param name="movie" value="' + swf + '" />'+
177
+ '<param name="allowscriptaccess" value="always" />'+
178
+ '</object>';
179
+ } else {
180
+ return '<object height="100%" width="100%" id="' + engineId + '">'+
181
+ '<embed allowscriptaccess="always" height="100%" width="100%" src="' + swf + '" type="application/x-shockwave-flash" name="' + engineId + '" />'+
182
+ '</object>';
183
+ }
184
+ };
185
+
186
+
187
+ // listen to audio engine events
188
+ // when the loaded track is ready to play
189
+ soundcloud.addEventListener('onPlayerReady', function(flashId, data) {
190
+ player = soundcloud.getPlayer(engineId);
191
+ callbacks.onReady();
192
+ });
193
+
194
+ // when the loaded track finished playing
195
+ soundcloud.addEventListener('onMediaEnd', callbacks.onEnd);
196
+
197
+ // when the loaded track is still buffering
198
+ soundcloud.addEventListener('onMediaBuffering', function(flashId, data) {
199
+ callbacks.onBuffer(data.percent);
200
+ });
201
+
202
+ // when the loaded track started to play
203
+ soundcloud.addEventListener('onMediaPlay', callbacks.onPlay);
204
+
205
+ // when the loaded track is was paused
206
+ soundcloud.addEventListener('onMediaPause', callbacks.onPause);
207
+
208
+ return {
209
+ load: function(track) {
210
+ var url = track.uri;
211
+ if(player){
212
+ player.api_load(url);
213
+ }else{
214
+ // create a container for the flash engine (IE needs this to operate properly)
215
+ $('<div class="sc-player-engine-container"></div>').appendTo(document.body).html(flashHtml(url));
216
+ }
217
+ },
218
+ play: function() {
219
+ player && player.api_play();
220
+ },
221
+ pause: function() {
222
+ player && player.api_pause();
223
+ },
224
+ stop: function(){
225
+ player && player.api_stop();
226
+ },
227
+ seek: function(relative){
228
+ player && player.api_seekTo((player.api_getTrackDuration() * relative));
229
+ },
230
+ getDuration: function() {
231
+ return player && player.api_getTrackDuration && player.api_getTrackDuration() * 1000;
232
+ },
233
+ getPosition: function() {
234
+ return player && player.api_getTrackPosition && player.api_getTrackPosition() * 1000;
235
+ },
236
+ setVolume: function(val) {
237
+ if(player && player.api_setVolume){
238
+ player.api_setVolume(val);
239
+ }
240
+ }
241
+
242
+ };
243
+ };
244
+
245
+ return html5AudioAvailable? html5Driver() : flashDriver();
246
+
247
+ }();
248
+
249
+
250
+
251
+ var apiKey,
252
+ didAutoPlay = false,
253
+ players = [],
254
+ updates = {},
255
+ currentUrl,
256
+ loadTracksData = function($player, links, key) {
257
+ var index = 0,
258
+ playerObj = {node: $player, tracks: []},
259
+ loadUrl = function(link) {
260
+ var apiUrl = scApiUrl(link.url, apiKey);
261
+ $.getJSON(apiUrl, function(data) {
262
+ // log('data loaded', link.url, data);
263
+ index += 1;
264
+ if(data.tracks){
265
+ // log('data.tracks', data.tracks);
266
+ playerObj.tracks = playerObj.tracks.concat(data.tracks);
267
+ }else if(data.duration){
268
+ // a secret link fix, till the SC API returns permalink with secret on secret response
269
+ data.permalink_url = link.url;
270
+ // if track, add to player
271
+ playerObj.tracks.push(data);
272
+ }else if(data.creator){
273
+ // it's a group!
274
+ links.push({url:data.uri + '/tracks'});
275
+ }else if(data.username){
276
+ // if user, get his tracks or favorites
277
+ if(/favorites/.test(link.url)){
278
+ links.push({url:data.uri + '/favorites'});
279
+ }else{
280
+ links.push({url:data.uri + '/tracks'});
281
+ }
282
+ }else if($.isArray(data)){
283
+ playerObj.tracks = playerObj.tracks.concat(data);
284
+ }
285
+ if(links[index]){
286
+ // if there are more track to load, get them from the api
287
+ loadUrl(links[index]);
288
+ }else{
289
+ // if loading finishes, anounce it to the GUI
290
+ playerObj.node.trigger({type:'onTrackDataLoaded', playerObj: playerObj, url: apiUrl});
291
+ }
292
+ });
293
+ };
294
+ // update current API key
295
+ apiKey = key;
296
+ // update the players queue
297
+ players.push(playerObj);
298
+ // load first tracks
299
+ loadUrl(links[index]);
300
+ },
301
+ artworkImage = function(track, usePlaceholder) {
302
+ if(usePlaceholder){
303
+ return '<div class="sc-loading-artwork">Loading Artwork</div>';
304
+ }else if (track.artwork_url) {
305
+ return '<img src="' + track.artwork_url.replace('-large', '-t300x300') + '"/>';
306
+ }else{
307
+ return '<div class="sc-no-artwork">No Artwork</div>';
308
+ }
309
+ },
310
+ updateTrackInfo = function($player, track) {
311
+ // update the current track info in the player
312
+ // log('updateTrackInfo', track);
313
+ $('.sc-info', $player).each(function(index) {
314
+ $('h3', this).html('<a href="' + track.permalink_url +'">' + track.title + '</a>');
315
+ $('h4', this).html('by <a href="' + track.user.permalink_url +'">' + track.user.username + '</a>');
316
+ $('p', this).html(track.description || 'no Description');
317
+ });
318
+ // update the artwork
319
+ $('.sc-artwork-list li', $player).each(function(index) {
320
+ var $item = $(this),
321
+ itemTrack = $item.data('sc-track');
322
+
323
+ if (itemTrack === track) {
324
+ // show track artwork
325
+ $item
326
+ .addClass('active')
327
+ .find('.sc-loading-artwork')
328
+ .each(function(index) {
329
+ // if the image isn't loaded yet, do it now
330
+ $(this).removeClass('sc-loading-artwork').html(artworkImage(track, false));
331
+ });
332
+ }else{
333
+ // reset other artworks
334
+ $item.removeClass('active');
335
+ }
336
+ });
337
+ // update the track duration in the progress bar
338
+ $('.sc-duration', $player).html(timecode(track.duration));
339
+ // put the waveform into the progress bar
340
+ $('.sc-waveform-container', $player).html('<img src="' + track.waveform_url +'" />');
341
+
342
+ $player.trigger('onPlayerTrackSwitch.scPlayer', [track]);
343
+ },
344
+ play = function(track) {
345
+ var url = track.permalink_url;
346
+ if(currentUrl === url){
347
+ // log('will play');
348
+ audioEngine.play();
349
+ }else{
350
+ currentUrl = url;
351
+ // log('will load', url);
352
+ audioEngine.load(track, apiKey);
353
+ }
354
+ },
355
+ getPlayerData = function(node) {
356
+ return players[$(node).data('sc-player').id];
357
+ },
358
+ updatePlayStatus = function(player, status) {
359
+ if(status){
360
+ // reset all other players playing status
361
+ $('div.sc-player.playing').removeClass('playing');
362
+ }
363
+ $(player)
364
+ .toggleClass('playing', status)
365
+ .trigger((status ? 'onPlayerPlay' : 'onPlayerPause'));
366
+ },
367
+ onPlay = function(player, id) {
368
+ var track = getPlayerData(player).tracks[id || 0];
369
+ updateTrackInfo(player, track);
370
+ // cache the references to most updated DOM nodes in the progress bar
371
+ updates = {
372
+ $buffer: $('.sc-buffer', player),
373
+ $played: $('.sc-played', player),
374
+ position: $('.sc-position', player)[0]
375
+ };
376
+ updatePlayStatus(player, true);
377
+ play(track);
378
+ },
379
+ onPause = function(player) {
380
+ updatePlayStatus(player, false);
381
+ audioEngine.pause();
382
+ },
383
+ onFinish = function() {
384
+ var $player = updates.$played.closest('.sc-player'),
385
+ $nextItem;
386
+ // update the scrubber width
387
+ updates.$played.css('width', '0%');
388
+ // show the position in the track position counter
389
+ updates.position.innerHTML = timecode(0);
390
+ // reset the player state
391
+ updatePlayStatus($player, false);
392
+ // stop the audio
393
+ audioEngine.stop();
394
+ $player.trigger('onPlayerTrackFinish');
395
+ },
396
+ onSeek = function(player, relative) {
397
+ audioEngine.seek(relative);
398
+ $(player).trigger('onPlayerSeek');
399
+ },
400
+ onSkip = function(player) {
401
+ var $player = $(player);
402
+ // continue playing through all players
403
+ log('track finished get the next one');
404
+ $nextItem = $('.sc-trackslist li.active', $player).next('li');
405
+ // try to find the next track in other player
406
+ if(!$nextItem.length){
407
+ $nextItem = $player.nextAll('div.sc-player:first').find('.sc-trackslist li.active');
408
+ }
409
+ $nextItem.click();
410
+ },
411
+ soundVolume = function() {
412
+ var vol = 80,
413
+ cooks = document.cookie.split(';'),
414
+ volRx = new RegExp('scPlayer_volume=(\\d+)');
415
+ for(var i in cooks){
416
+ if(volRx.test(cooks[i])){
417
+ vol = parseInt(cooks[i].match(volRx)[1], 10);
418
+ break;
419
+ }
420
+ }
421
+ return vol;
422
+ }(),
423
+ onVolume = function(volume) {
424
+ var vol = Math.floor(volume);
425
+ // save the volume in the cookie
426
+ var date = new Date();
427
+ date.setTime(date.getTime() + (365 * 24 * 60 * 60 * 1000));
428
+ soundVolume = vol;
429
+ document.cookie = ['scPlayer_volume=', vol, '; expires=', date.toUTCString(), '; path="/"'].join('');
430
+ // update the volume in the engine
431
+ audioEngine.setVolume(soundVolume);
432
+ },
433
+ positionPoll;
434
+
435
+ // listen to audio engine events
436
+ $doc
437
+ .bind('scPlayer:onAudioReady', function(event) {
438
+ log('onPlayerReady: audio engine is ready');
439
+ audioEngine.play();
440
+ // set initial volume
441
+ onVolume(soundVolume);
442
+ })
443
+ // when the loaded track started to play
444
+ .bind('scPlayer:onMediaPlay', function(event) {
445
+ clearInterval(positionPoll);
446
+ positionPoll = setInterval(function() {
447
+ var duration = audioEngine.getDuration(),
448
+ position = audioEngine.getPosition(),
449
+ relative = (position / duration);
450
+
451
+ // update the scrubber width
452
+ updates.$played.css('width', (100 * relative) + '%');
453
+ // show the position in the track position counter
454
+ updates.position.innerHTML = timecode(position);
455
+ // announce the track position to the DOM
456
+ $doc.trigger({
457
+ type: 'onMediaTimeUpdate.scPlayer',
458
+ duration: duration,
459
+ position: position,
460
+ relative: relative
461
+ });
462
+ }, 500);
463
+ })
464
+ // when the loaded track is was paused
465
+ .bind('scPlayer:onMediaPause', function(event) {
466
+ clearInterval(positionPoll);
467
+ positionPoll = null;
468
+ })
469
+ // change the volume
470
+ .bind('scPlayer:onVolumeChange', function(event) {
471
+ onVolume(event.volume);
472
+ })
473
+ .bind('scPlayer:onMediaEnd', function(event) {
474
+ onFinish();
475
+ })
476
+ .bind('scPlayer:onMediaBuffering', function(event) {
477
+ updates.$buffer.css('width', event.percent + '%');
478
+ });
479
+
480
+
481
+ // Generate custom skinnable HTML/CSS/JavaScript based SoundCloud players from links to SoundCloud resources
482
+ $.scPlayer = function(options, node) {
483
+ var opts = $.extend({}, $.scPlayer.defaults, options),
484
+ playerId = players.length,
485
+ $source = node && $(node),
486
+ sourceClasses = $source[0].className.replace('sc-player', ''),
487
+ links = opts.links || $.map($('a', $source).add($source.filter('a')), function(val) { return {url: val.href, title: val.innerHTML}; }),
488
+ $player = $('<div class="sc-player loading"></div>').data('sc-player', {id: playerId}),
489
+ $artworks = $('<ol class="sc-artwork-list"></ol>').appendTo($player),
490
+ $info = $('<div class="sc-info"><h3></h3><h4></h4><p></p><a href="#" class="sc-info-close">X</a></div>').appendTo($player),
491
+ $controls = $('<div class="sc-controls"></div>').appendTo($player),
492
+ $list = $('<ol class="sc-trackslist"></ol>').appendTo($player);
493
+
494
+ // add the classes of the source node to the player itself
495
+ // the players can be indvidually styled this way
496
+ if(sourceClasses || opts.customClass){
497
+ $player.addClass(sourceClasses).addClass(opts.customClass);
498
+ }
499
+
500
+
501
+ // adding controls to the player
502
+ $player
503
+ .find('.sc-controls')
504
+ .append('<a href="#play" class="sc-play">Play</a> <a href="#pause" class="sc-pause hidden">Pause</a>')
505
+ .end()
506
+ .append('<a href="#info" class="sc-info-toggle">Info</a>')
507
+ .append('<div class="sc-scrubber"></div>')
508
+ .find('.sc-scrubber')
509
+ .append('<div class="sc-volume-slider"><span class="sc-volume-status" style="width:' + soundVolume +'%"></span></div>')
510
+ .append('<div class="sc-time-span"><div class="sc-waveform-container"></div><div class="sc-buffer"></div><div class="sc-played"></div></div>')
511
+ .append('<div class="sc-time-indicators"><span class="sc-position"></span> | <span class="sc-duration"></span></div>');
512
+
513
+ // load and parse the track data from SoundCloud API
514
+ loadTracksData($player, links, opts.apiKey);
515
+ // init the player GUI, when the tracks data was laoded
516
+ $player.bind('onTrackDataLoaded.scPlayer', function(event) {
517
+ // log('onTrackDataLoaded.scPlayer', event.playerObj, playerId, event.target);
518
+ var tracks = event.playerObj.tracks;
519
+ if (opts.randomize) {
520
+ tracks = shuffle(tracks);
521
+ }
522
+ // create the playlist
523
+ $.each(tracks, function(index, track) {
524
+ var active = index === 0;
525
+ // create an item in the playlist
526
+ $('<li><a href="' + track.permalink_url +'">' + track.title + '</a><span class="sc-track-duration">' + timecode(track.duration) + '</span></li>').data('sc-track', {id:index}).toggleClass('active', active).appendTo($list);
527
+ // create an item in the artwork list
528
+ $('<li></li>')
529
+ .append(artworkImage(track, index >= opts.loadArtworks))
530
+ .appendTo($artworks)
531
+ .toggleClass('active', active)
532
+ .data('sc-track', track);
533
+ });
534
+ // update the element before rendering it in the DOM
535
+ $player.each(function() {
536
+ if($.isFunction(opts.beforeRender)){
537
+ opts.beforeRender.call(this, tracks);
538
+ }
539
+ });
540
+ // set the first track's duration
541
+ $('.sc-duration', $player)[0].innerHTML = timecode(tracks[0].duration);
542
+ $('.sc-position', $player)[0].innerHTML = timecode(0);
543
+ // set up the first track info
544
+ updateTrackInfo($player, tracks[0]);
545
+
546
+ // if continous play enabled always skip to the next track after one finishes
547
+ if (opts.continuePlayback) {
548
+ $player.bind('onPlayerTrackFinish', function(event) {
549
+ onSkip($player);
550
+ });
551
+ }
552
+
553
+ // announce the succesful initialization
554
+ $player
555
+ .removeClass('loading')
556
+ .trigger('onPlayerInit');
557
+
558
+ // if auto play is enabled and it's the first player, start playing
559
+ if(opts.autoPlay && !didAutoPlay){
560
+ onPlay($player);
561
+ didAutoPlay = true;
562
+ }
563
+ });
564
+
565
+
566
+ // replace the DOM source (if there's one)
567
+ $source.each(function(index) {
568
+ $(this).replaceWith($player);
569
+ });
570
+
571
+ return $player;
572
+ };
573
+
574
+ // stop all players, might be useful, before replacing the player dynamically
575
+ $.scPlayer.stopAll = function() {
576
+ $('.sc-player.playing a.sc-pause').click();
577
+ };
578
+
579
+ // destroy all the players and audio engine, usefull when reloading part of the page and audio has to stop
580
+ $.scPlayer.destroy = function() {
581
+ $('.sc-player, .sc-player-engine-container').remove();
582
+ };
583
+
584
+ // plugin wrapper
585
+ $.fn.scPlayer = function(options) {
586
+ // reset the auto play
587
+ didAutoPlay = false;
588
+ // create the players
589
+ this.each(function() {
590
+ $.scPlayer(options, this);
591
+ });
592
+ return this;
593
+ };
594
+
595
+ // default plugin options
596
+ $.scPlayer.defaults = $.fn.scPlayer.defaults = {
597
+ customClass: null,
598
+ // do something with the dom object before you render it, add nodes, get more data from the services etc.
599
+ beforeRender : function(tracksData) {
600
+ var $player = $(this);
601
+ },
602
+ // initialization, when dom is ready
603
+ onDomReady : function() {
604
+ $('a.sc-player, div.sc-player').scPlayer();
605
+ },
606
+ autoPlay: false,
607
+ continuePlayback: true,
608
+ randomize: false,
609
+ loadArtworks: 5,
610
+ // the default Api key should be replaced by your own one
611
+ // get it here http://soundcloud.com/you/apps/new
612
+ apiKey: 'htuiRd1JP11Ww0X72T1C3g'
613
+ };
614
+
615
+
616
+ // the GUI event bindings
617
+ //--------------------------------------------------------
618
+
619
+ // toggling play/pause
620
+ $('a.sc-play, a.sc-pause').live('click', function(event) {
621
+ var $list = $(this).closest('.sc-player').find('ol.sc-trackslist');
622
+ // simulate the click in the tracklist
623
+ $list.find('li.active').click();
624
+ return false;
625
+ });
626
+
627
+ // displaying the info panel in the player
628
+ $('a.sc-info-toggle, a.sc-info-close').live('click', function(event) {
629
+ var $link = $(this);
630
+ $link.closest('.sc-player')
631
+ .find('.sc-info').toggleClass('active').end()
632
+ .find('a.sc-info-toggle').toggleClass('active');
633
+ return false;
634
+ });
635
+
636
+ // selecting tracks in the playlist
637
+ $('.sc-trackslist li').live('click', function(event) {
638
+ var $track = $(this),
639
+ $player = $track.closest('.sc-player'),
640
+ trackId = $track.data('sc-track').id,
641
+ play = $player.is(':not(.playing)') || $track.is(':not(.active)');
642
+ if (play) {
643
+ onPlay($player, trackId);
644
+ }else{
645
+ onPause($player);
646
+ }
647
+ $track.addClass('active').siblings('li').removeClass('active');
648
+ $('.artworks li', $player).each(function(index) {
649
+ $(this).toggleClass('active', index === trackId);
650
+ });
651
+ return false;
652
+ });
653
+
654
+ var scrub = function(node, xPos) {
655
+ var $scrubber = $(node).closest('.sc-time-span'),
656
+ $buffer = $scrubber.find('.sc-buffer'),
657
+ $available = $scrubber.find('.sc-waveform-container img'),
658
+ $player = $scrubber.closest('.sc-player'),
659
+ relative = Math.min($buffer.width(), (xPos - $available.offset().left)) / $available.width();
660
+ onSeek($player, relative);
661
+ };
662
+
663
+ var onTouchMove = function(ev) {
664
+ if (ev.targetTouches.length === 1) {
665
+ scrub(ev.target, ev.targetTouches && ev.targetTouches.length && ev.targetTouches[0].clientX);
666
+ ev.preventDefault();
667
+ }
668
+ };
669
+
670
+
671
+ // seeking in the loaded track buffer
672
+ $('.sc-time-span')
673
+ .live('click', function(event) {
674
+ scrub(this, event.pageX);
675
+ return false;
676
+ })
677
+ .live('touchstart', function(event) {
678
+ this.addEventListener('touchmove', onTouchMove, false);
679
+ event.originalEvent.preventDefault();
680
+ })
681
+ .live('touchend', function(event) {
682
+ this.removeEventListener('touchmove', onTouchMove, false);
683
+ event.originalEvent.preventDefault();
684
+ });
685
+
686
+ // changing volume in the player
687
+ var startVolumeTracking = function(node, startEvent) {
688
+ var $node = $(node),
689
+ originX = $node.offset().left,
690
+ originWidth = $node.width(),
691
+ getVolume = function(x) {
692
+ return Math.floor(((x - originX)/originWidth)*100);
693
+ },
694
+ update = function(event) {
695
+ $doc.trigger({type: 'scPlayer:onVolumeChange', volume: getVolume(event.pageX)});
696
+ };
697
+ $node.bind('mousemove.sc-player', update);
698
+ update(startEvent);
699
+ };
700
+
701
+ var stopVolumeTracking = function(node, event) {
702
+ $(node).unbind('mousemove.sc-player');
703
+ };
704
+
705
+ $('.sc-volume-slider')
706
+ .live('mousedown', function(event) {
707
+ startVolumeTracking(this, event);
708
+ })
709
+ .live('mouseup', function(event) {
710
+ stopVolumeTracking(this, event);
711
+ });
712
+
713
+ $doc.bind('scPlayer:onVolumeChange', function(event) {
714
+ $('span.sc-volume-status').css({width: event.volume + '%'});
715
+ });
716
+ // -------------------------------------------------------------------
717
+
718
+ // the default Auto-Initialization
719
+ $(function() {
720
+ if($.isFunction($.scPlayer.defaults.onDomReady)){
721
+ $.scPlayer.defaults.onDomReady();
722
+ }
723
+ });
724
+
725
+ })(jQuery);