/*
* SoundCloud Custom Player jQuery Plugin
* Author: Matas Petrikas, matas@soundcloud.com
* Copyright (c) 2009 SoundCloud Ltd.
* Licensed under the MIT license:
* http://www.opensource.org/licenses/mit-license.php
*
* Usage:
* My new dub track
* The link will be automatically replaced by the HTML based player
*/
(function($) {
// Convert milliseconds into Hours (h), Minutes (m), and Seconds (s)
var timecode = function(ms) {
var hms = function(ms) {
return {
h: Math.floor(ms/(60*60*1000)),
m: Math.floor((ms/60000) % 60),
s: Math.floor((ms/1000) % 60)
};
}(ms),
tc = []; // Timecode array to be joined with '.'
if (hms.h > 0) {
tc.push(hms.h);
}
tc.push((hms.m < 10 && hms.h > 0 ? "0" + hms.m : hms.m));
tc.push((hms.s < 10 ? "0" + hms.s : hms.s));
return tc.join(':');
};
// shuffle the array
var shuffle = function(arr) {
arr.sort(function() { return 1 - Math.floor(Math.random() * 3); } );
return arr;
};
var debug = true,
useSandBox = false,
$doc = $(document),
log = function(args) {
try {
if(debug && window.console && window.console.log){
window.console.log.apply(window.console, arguments);
}
} catch (e) {
// no console available
}
},
domain = useSandBox ? 'sandbox-soundcloud.com' : 'soundcloud.com',
secureDocument = (document.location.protocol === 'https:'),
// convert a SoundCloud resource URL to an API URL
scApiUrl = function(url, apiKey) {
// var resolver = ( secureDocument || (/^https/i).test(url) ? 'https' : 'http') + '://api.' + domain + '/resolve?url=',
// params = 'format=json&consumer_key=' + apiKey +'&callback=?';
var resolver = ( secureDocument || (/^https/i).test(url) ? 'https' : 'http') + '://api.' + domain + '/resolve?url=',
params = 'format=json&callback=?';
// debugger;
// force the secure url in the secure environment
if( secureDocument ) {
url = url.replace(/^http:/, 'https:');
}
// check if it's already a resolved api url
if ( (/api\./).test(url) ) {
return url + '?' + params;
} else {
return resolver + url + '&' + params;
}
};
// TODO Expose the audio engine, so it can be unit-tested
var audioEngine = function() {
var html5AudioAvailable = function() {
var state = false;
try{
var a = new Audio();
state = a.canPlayType && (/maybe|probably/).test(a.canPlayType('audio/mpeg'));
// uncomment the following line, if you want to enable the html5 audio only on mobile devices
// state = state && (/iPad|iphone|mobile|pre\//i).test(navigator.userAgent);
}catch(e){
// there's no audio support here sadly
}
return state;
}(),
callbacks = {
onReady: function() {
$doc.trigger('paScPlayer:onAudioReady');
},
onPlay: function() {
$doc.trigger('paScPlayer:onMediaPlay');
},
onPause: function() {
$doc.trigger('paScPlayer:onMediaPause');
},
onEnd: function() {
$doc.trigger('paScPlayer:onMediaEnd');
},
onBuffer: function(percent) {
$doc.trigger({type: 'paScPlayer:onMediaBuffering', percent: percent});
}
};
var html5Driver = function() {
var player = new Audio(),
onTimeUpdate = function(event){
var obj = event.target,
buffer = ((obj.buffered.length && obj.buffered.end(0)) / obj.duration) * 100;
// ipad has no progress events implemented yet
callbacks.onBuffer(buffer);
// anounce if it's finished for the clients without 'ended' events implementation
if (obj.currentTime === obj.duration) { callbacks.onEnd(); }
},
onProgress = function(event) {
var obj = event.target,
buffer = ((obj.buffered.length && obj.buffered.end(0)) / obj.duration) * 100;
callbacks.onBuffer(buffer);
};
$('
').appendTo(document.body).append(player);
// prepare the listeners
player.addEventListener('play', callbacks.onPlay, false);
player.addEventListener('pause', callbacks.onPause, false);
// handled in the onTimeUpdate for now untill all the browsers support 'ended' event
// player.addEventListener('ended', callbacks.onEnd, false);
player.addEventListener('timeupdate', onTimeUpdate, false);
player.addEventListener('progress', onProgress, false);
return {
load: function(track, apiKey) {
player.pause();
player.src = track.stream_url + (/\?/.test(track.stream_url) ? '&' : '?') + 'consumer_key=' + apiKey;
player.load();
player.play();
},
play: function() {
player.play();
},
pause: function() {
player.pause();
},
stop: function(){
if (player.currentTime) {
player.currentTime = 0;
player.pause();
}
},
seek: function(relative){
player.currentTime = player.duration * relative;
player.play();
},
getDuration: function() {
return player.duration * 1000;
},
getPosition: function() {
return player.currentTime * 1000;
},
setVolume: function(val) {
player.volume = val / 100;
}
};
};
var flashDriver = function() {
var engineId = 'paScPlayerEngine',
player,
flashHtml = function(url) {
var swf = (secureDocument ? 'https' : 'http') + '://player.' + domain +'/player.swf?url=' + url +'&enable_api=true&player_type=engine&object_id=' + engineId;
if ($.browser.msie) {
return ''+
' '+
' '+
' ';
} else {
return ''+
' '+
' ';
}
};
// listen to audio engine events
// when the loaded track is ready to play
soundcloud.addEventListener('onPlayerReady', function(flashId, data) {
player = soundcloud.getPlayer(engineId);
callbacks.onReady();
});
// when the loaded track finished playing
soundcloud.addEventListener('onMediaEnd', callbacks.onEnd);
// when the loaded track is still buffering
soundcloud.addEventListener('onMediaBuffering', function(flashId, data) {
callbacks.onBuffer(data.percent);
});
// when the loaded track started to play
soundcloud.addEventListener('onMediaPlay', callbacks.onPlay);
// when the loaded track is was paused
soundcloud.addEventListener('onMediaPause', callbacks.onPause);
return {
load: function(track) {
var url = track.uri;
if(player){
player.api_load(url);
}else{
// create a container for the flash engine (IE needs this to operate properly)
$('
').appendTo(document.body).html(flashHtml(url));
}
},
play: function() {
player && player.api_play();
},
pause: function() {
player && player.api_pause();
},
stop: function(){
player && player.api_stop();
},
seek: function(relative){
player && player.api_seekTo((player.api_getTrackDuration() * relative));
},
getDuration: function() {
return player && player.api_getTrackDuration && player.api_getTrackDuration() * 1000;
},
getPosition: function() {
return player && player.api_getTrackPosition && player.api_getTrackPosition() * 1000;
},
setVolume: function(val) {
if(player && player.api_setVolume){
player.api_setVolume(val);
}
}
};
};
return html5AudioAvailable? html5Driver() : flashDriver();
}();
var apiKey,
didAutoPlay = false,
players = [],
updates = {},
currentUrl,
loadTracksData = function($player, links, key) {
var index = 0,
playerObj = {node: $player, tracks: []},
loadUrl = function(link) {
var apiUrl = scApiUrl(link.url, apiKey);
// $.getJSON(apiUrl, function(data) {
// // log('data loaded', link.url, data);
// index += 1;
// // added by Bach for MJ
// data.href = link.href;
// if(data.tracks){
// // log('data.tracks', data.tracks);
// playerObj.tracks = playerObj.tracks.concat(data.tracks);
// }else if(data.duration){
// // a secret link fix, till the SC API returns permalink with secret on secret response
// data.permalink_url = link.url;
// // if track, add to player
// playerObj.tracks.push(data);
// }else if(data.creator){
// // it's a group!
// links.push({url:data.uri + '/tracks'});
// }else if(data.username){
// // if user, get his tracks or favorites
// if(/favorites/.test(link.url)){
// links.push({url:data.uri + '/favorites'});
// }else{
// links.push({url:data.uri + '/tracks'});
// }
// }else if($.isArray(data)){
// playerObj.tracks = playerObj.tracks.concat(data);
// }
// if(links[index]){
// // if there are more track to load, get them from the api
// loadUrl(links[index]);
// }else{
// // if loading finishes, anounce it to the GUI
// playerObj.node.trigger({type:'onTrackDataLoaded', playerObj: playerObj, url: apiUrl});
// }
// });
// https://developers.soundcloud.com/docs/api/guide#authentication
// # obtain the access token
// $ curl -X POST "https://api.soundcloud.com/oauth2/token" \
// -H "accept: application/json; charset=utf-8" \
// -H "Content-Type: application/x-www-form-urlencoded" \
// --data-urlencode "grant_type=client_credentials" \
// --data-urlencode "client_id=YOUR_CLIENT_ID" \
// --data-urlencode "client_secret=YOUR_CLIENT_SECRET"
$.ajax({
method: 'POST',
url: 'https://api.soundcloud.com/oauth2/token',
data: {
'grant_type' : "client_credentials",
'client_id' : "965bd4363fdd909723749b003be67125",
'client_secret': "bb68647335a47f104a86dcddf4e70fa8"
},
headers: {
"accept" : "application/json; charset=utf-8",
"Content-Type" : "application/x-www-form-urlencoded"
}
}).done(function (data) {
console.log('Token', data);
});
// $.ajax({
// "dataType": "json",
// "url": apiUrl,
// "data": {},
// "headers": {
// "Authorization": "OAuth bb68647335a47f104a86dcddf4e70fa8"
// },
// "success": function(data) {
// // log('data loaded', link.url, data);
// index += 1;
// // added by Bach for MJ
// data.href = link.href;
// if(data.tracks){
// // log('data.tracks', data.tracks);
// playerObj.tracks = playerObj.tracks.concat(data.tracks);
// }else if(data.duration){
// // a secret link fix, till the SC API returns permalink with secret on secret response
// data.permalink_url = link.url;
// // if track, add to player
// playerObj.tracks.push(data);
// }else if(data.creator){
// // it's a group!
// links.push({url:data.uri + '/tracks'});
// }else if(data.username){
// // if user, get his tracks or favorites
// if(/favorites/.test(link.url)){
// links.push({url:data.uri + '/favorites'});
// }else{
// links.push({url:data.uri + '/tracks'});
// }
// }else if($.isArray(data)){
// playerObj.tracks = playerObj.tracks.concat(data);
// }
// if(links[index]){
// // if there are more track to load, get them from the api
// loadUrl(links[index]);
// }else{
// // if loading finishes, anounce it to the GUI
// playerObj.node.trigger({type:'onTrackDataLoaded', playerObj: playerObj, url: apiUrl});
// }
// },
// "error": function(errorThrown) {
// console.error(JSON.stringify(errorThrown.error()));
// }
// });
};
// update current API key
apiKey = key;
// update the players queue
players.push(playerObj);
// load first tracks
loadUrl(links[index]);
},
artworkImage = function(track, usePlaceholder) {
if(usePlaceholder){
return 'Loading Artwork
';
}else if (track.artwork_url) {
return ' ';
}else{
return 'No Artwork
';
}
},
updateTrackInfo = function($player, track) {
// update the current track info in the player
// log('updateTrackInfo', track);
// $('.sc-info', $player).each(function(index) {
// $('h3', this).html('' + track.title + ' ');
// $('h4', this).html('by ' + track.user.username + ' ');
// $('p', this).html(track.description || 'no Description');
// });
// update the artwork
// $('.sc-artwork-list li', $player).each(function(index) {
// var $item = $(this),
// itemTrack = $item.data('sc-track');
//
// if (itemTrack === track) {
// // show track artwork
// $item
// .addClass('active')
// .find('.sc-loading-artwork')
// .each(function(index) {
// // if the image isn't loaded yet, do it now
// $(this).removeClass('sc-loading-artwork').html(artworkImage(track, false));
// });
// }else{
// // reset other artworks
// $item.removeClass('active');
// }
// });
// update the track duration in the progress bar
$('.sc-duration', $player).html(timecode(track.duration));
// put the waveform into the progress bar
$('.sc-waveform-container', $player).html(' ');
$player.trigger('onPlayerTrackSwitch.paScPlayer', [track]);
},
play = function(track) {
var url = track.permalink_url;
if(currentUrl === url){
// log('will play');
audioEngine.play();
}else{
currentUrl = url;
// log('will load', url);
audioEngine.load(track, apiKey);
}
},
getPlayerData = function(node) {
return players[$(node).data('pa-sc-player').id];
},
updatePlayStatus = function(player, status) {
if(status){
// reset all other players playing status
$('div.pa-sc-player.playing').removeClass('playing');
}
$(player)
.toggleClass('playing', status)
.trigger((status ? 'onPlayerPlay' : 'onPlayerPause'));
},
onPlay = function(player, id) {
var track = getPlayerData(player).tracks[id || 0];
updateTrackInfo(player, track);
// cache the references to most updated DOM nodes in the progress bar
updates = {
$buffer: $('.sc-buffer', player),
$played: $('.sc-played', player),
position: $('.sc-position', player)[0]
};
updatePlayStatus(player, true);
play(track);
},
onPause = function(player) {
updatePlayStatus(player, false);
audioEngine.pause();
},
onFinish = function() {
var $player = updates.$played.closest('.pa-sc-player'),
$nextItem;
// update the scrubber width
updates.$played.css('width', '0%');
// show the position in the track position counter
updates.position.innerHTML = timecode(0);
// reset the player state
updatePlayStatus($player, false);
// stop the audio
audioEngine.stop();
$player.trigger('onPlayerTrackFinish');
},
onSeek = function(player, relative) {
audioEngine.seek(relative);
$(player).trigger('onPlayerSeek');
},
onSkip = function(player) {
var $player = $(player);
// continue playing through all players
log('track finished get the next one');
$nextItem = $('.sc-trackslist li.active', $player).next('li');
// try to find the next track in other player
if(!$nextItem.length){
$nextItem = $player.nextAll('div.pa-sc-player:first').find('.sc-trackslist li.active');
}
$nextItem.click();
},
soundVolume = function() {
var vol = 80,
cooks = document.cookie.split(';'),
volRx = new RegExp('paScPlayer_volume=(\\d+)');
for(var i in cooks){
if(volRx.test(cooks[i])){
vol = parseInt(cooks[i].match(volRx)[1], 10);
break;
}
}
return vol;
}(),
onVolume = function(volume) {
var vol = Math.floor(volume);
// save the volume in the cookie
var date = new Date();
date.setTime(date.getTime() + (365 * 24 * 60 * 60 * 1000));
soundVolume = vol;
document.cookie = ['paScPlayer_volume=', vol, '; expires=', date.toUTCString(), '; path="/"'].join('');
// update the volume in the engine
audioEngine.setVolume(soundVolume);
},
positionPoll;
// listen to audio engine events
$doc
.bind('paScPlayer:onAudioReady', function(event) {
log('onPlayerReady: audio engine is ready');
audioEngine.play();
// set initial volume
onVolume(soundVolume);
})
// when the loaded track started to play
.bind('paScPlayer:onMediaPlay', function(event) {
clearInterval(positionPoll);
positionPoll = setInterval(function() {
var duration = audioEngine.getDuration(),
position = audioEngine.getPosition(),
relative = (position / duration);
// update the scrubber width
updates.$played.css('width', (100 * relative) + '%');
// show the position in the track position counter
updates.position.innerHTML = timecode(position);
// announce the track position to the DOM
$doc.trigger({
type: 'onMediaTimeUpdate.paScPlayer',
duration: duration,
position: position,
relative: relative
});
}, 500);
})
// when the loaded track is was paused
.bind('paScPlayer:onMediaPause', function(event) {
clearInterval(positionPoll);
positionPoll = null;
})
// change the volume
// .bind('paScPlayer:onVolumeChange', function(event) {
// onVolume(event.volume);
// })
.bind('paScPlayer:onMediaEnd', function(event) {
onFinish();
})
.bind('paScPlayer:onMediaBuffering', function(event) {
updates.$buffer.css('width', event.percent + '%');
});
// Generate custom skinnable HTML/CSS/JavaScript based SoundCloud players from links to SoundCloud resources
$.paScPlayer = function(options, node) {
var opts = $.extend({}, $.paScPlayer.defaults, options),
playerId = players.length,
$source = node && $(node),
sourceClasses = $source[0].className.replace('pa-sc-player', ''),
links =
opts.links
|| $.map($('a', $source)
.add($source.filter('a')), function(val) {
//log('val', val);
return {href: val.href, url: $(val).attr('scurl') || val.href, title: val.innerHTML};
// return {url: val.href, title: val.innerHTML};
}),
$player = $('
').data('pa-sc-player', {id: playerId}),
// $artworks = $(' ').appendTo($player),
// $info = $('').appendTo($player),
$list = $('').appendTo($player),
$controls = $('
').appendTo($player);
// add the classes of the source node to the player itself
// the players can be indvidually styled this way
if(sourceClasses || opts.customClass){
$player.addClass(sourceClasses).addClass(opts.customClass);
}
log('$source', $source);
log('links', links);
// adding controls to the player
$player
.find('.sc-controls')
.append('play pause ')
.end()
// .append('Info ')
// .append('
')
// .find('.sc-scrubber')
// .append('
')
.append('00.00 |
')
.append(''); //
// load and parse the track data from SoundCloud API
loadTracksData($player, links, opts.apiKey);
// init the player GUI, when the tracks data was laoded
$player.bind('onTrackDataLoaded.paScPlayer', function(event) {
log('onTrackDataLoaded.paScPlayer', event, playerId);
// var tracks = event.playerObj.tracks;
// if (opts.randomize) {
// tracks = shuffle(tracks);
// }
// create the playlist
$.each(event.playerObj.tracks, function(index, track) {
log('track', track);
var active = index === 0;
// create an item in the playlist
$('')
.append($('').attr('href', track.href).html(links[index].title))
.data('sc-track', {id:index})
.toggleClass('active', active)
.appendTo($list);
// create an item in the artwork list
// $(' ')
// .append(artworkImage(track, index >= opts.loadArtworks))
// .appendTo($artworks)
// .toggleClass('active', active)
// .data('sc-track', track);
});
// update the element before rendering it in the DOM
$player.each(function() {
if($.isFunction(opts.beforeRender)){
opts.beforeRender.call(this, event.playerObj.tracks);
}
});
// set the first track's duration
$('.sc-duration', $player)[0].innerHTML = timecode(event.playerObj.tracks[0].duration);
$('.sc-position', $player)[0].innerHTML = timecode(0);
// set up the first track info
updateTrackInfo($player, event.playerObj.tracks[0]);
// if continous play enabled always skip to the next track after one finishes
if (opts.continuePlayback) {
$player.bind('onPlayerTrackFinish', function(event) {
onSkip($player);
});
}
// announce the succesful initialization
$player
.removeClass('loading')
.trigger('onPlayerInit');
// if auto play is enabled and it's the first player, start playing
if(opts.autoPlay && !didAutoPlay){
onPlay($player);
didAutoPlay = true;
}
});
// replace the DOM source (if there's one)
$source.each(function(index) {
$(this).replaceWith($player);
});
return $player;
};
// stop all players, might be useful, before replacing the player dynamically
$.paScPlayer.stopAll = function() {
$('.pa-sc-player.playing a.sc-pause').click();
};
// destroy all the players and audio engine, usefull when reloading part of the page and audio has to stop
$.paScPlayer.destroy = function() {
$('.pa-sc-player, .pa-sc-player-engine-container').remove();
};
// plugin wrapper
$.fn.paScPlayer = function(options) {
// reset the auto play
didAutoPlay = false;
// create the players
this.each(function() {
$.paScPlayer(options, this);
});
return this;
};
// default plugin options
$.paScPlayer.defaults = $.fn.paScPlayer.defaults = {
customClass: null,
// do something with the dom object before you render it, add nodes, get more data from the services etc.
beforeRender : function(tracksData) {
var $player = $(this);
},
// initialization, when dom is ready
onDomReady : function() {
$('a.pa-sc-player, div.pa-sc-player').paScPlayer();
},
autoPlay: false,
continuePlayback: true,
randomize: false,
loadArtworks: 5,
// the default Api key should be replaced by your own one
// get it here http://soundcloud.com/you/apps/new
apiKey: '965bd4363fdd909723749b003be67125'
};
// the GUI event bindings
//--------------------------------------------------------
// toggling play/pause
$('a.sc-play, a.sc-pause').live('click', function(event) {
var $list = $(this).closest('.pa-sc-player').find('ul.sc-trackslist');
// simulate the click in the tracklist
$list.find('li.active').click();
return false;
});
// displaying the info panel in the player
// $('a.sc-info-toggle, a.sc-info-close').live('click', function(event) {
// var $link = $(this);
// $link.closest('.pa-sc-player')
// .find('.sc-info').toggleClass('active').end()
// .find('a.sc-info-toggle').toggleClass('active');
// return false;
// });
// selecting tracks in the playlist
$('.sc-trackslist li').live('click', function(event) {
var $track = $(this),
$player = $track.closest('.pa-sc-player'),
trackId = $track.data('sc-track').id,
play = $player.is(':not(.playing)') || $track.is(':not(.active)');
if (play) {
onPlay($player, trackId);
}else{
onPause($player);
}
$track.addClass('active').siblings('li').removeClass('active');
// $('.artworks li', $player).each(function(index) {
// $(this).toggleClass('active', index === trackId);
// });
return false;
});
var scrub = function(node, xPos) {
var $scrubber = $(node).closest('.sc-time-span'),
$buffer = $scrubber.find('.sc-buffer'),
// $available = $scrubber.find('.sc-waveform-container img'),
$player = $scrubber.closest('.pa-sc-player'),
relative = Math.min($buffer.width(), (xPos - $scrubber.offset().left)) / $scrubber.width();
onSeek($player, relative);
};
var onTouchMove = function(ev) {
if (ev.targetTouches.length === 1) {
scrub(ev.target, ev.targetTouches && ev.targetTouches.length && ev.targetTouches[0].clientX);
ev.preventDefault();
}
};
// seeking in the loaded track buffer
$('.sc-time-span')
.live('click', function(event) {
scrub(this, event.pageX);
return false;
})
.live('touchstart', function(event) {
this.addEventListener('touchmove', onTouchMove, false);
event.originalEvent.preventDefault();
})
.live('touchend', function(event) {
this.removeEventListener('touchmove', onTouchMove, false);
event.originalEvent.preventDefault();
});
// changing volume in the player
// var startVolumeTracking = function(node, startEvent) {
// var $node = $(node),
// originX = $node.offset().left,
// originWidth = $node.width(),
// getVolume = function(x) {
// return Math.floor(((x - originX)/originWidth)*100);
// },
// update = function(event) {
// $doc.trigger({type: 'paScPlayer:onVolumeChange', volume: getVolume(event.pageX)});
// };
// $node.bind('mousemove.pa-sc-player', update);
// update(startEvent);
// };
// var stopVolumeTracking = function(node, event) {
// $(node).unbind('mousemove.pa-sc-player');
// };
// $('.sc-volume-slider')
// .live('mousedown', function(event) {
// startVolumeTracking(this, event);
// })
// .live('mouseup', function(event) {
// stopVolumeTracking(this, event);
// });
// $doc.bind('paScPlayer:onVolumeChange', function(event) {
// $('span.sc-volume-status').css({width: event.volume + '%'});
// });
// -------------------------------------------------------------------
// the default Auto-Initialization
$(function() {
if($.isFunction($.paScPlayer.defaults.onDomReady)){
$.paScPlayer.defaults.onDomReady();
}
});
})(jQuery);