/* * 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 = $('

    X
    ').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 $('
  1. ') .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 // $('
  2. ') // .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);