/* * 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 Math.round(Math.random()); } ); return arr; }; var debug = true, useSandBox = false, $doc = $(document), log = function(args) { if(debug && window.console && window.console.log){ window.console.log.apply(window.console, arguments); } }, domain = useSandBox ? 'sandbox-soundcloud.com' : 'soundcloud.com', scApiUrl = function(url, apiKey) { return (/api\./.test(url) ? url + '?' : 'http://api.' + domain +'/resolve?url=' + url + '&') + 'format=json&consumer_key=' + apiKey +'&callback=?'; }; var audioEngine = function() { var html5AudioAvailable = function() { var state = false; try{ var a = new Audio(); state = a.canPlayType && (/maybe|probably/).test(a.canPlayType('audio/mpeg')); // let's enable the html5 audio on selected mobile devices first, unlikely to support Flash // the desktop browsers are still better with Flash, e.g. see the Safari 10.6 bug // comment the following line out, if you want to force the html5 mode 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('scPlayer:onAudioReady'); }, onPlay: function() { $doc.trigger('scPlayer:onMediaPlay'); }, onPause: function() { $doc.trigger('scPlayer:onMediaPause'); }, onEnd: function() { $doc.trigger('scPlayer:onMediaEnd'); }, onBuffer: function(percent) { $doc.trigger({type: 'scPlayer: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); 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 + '?consumer_key=' + apiKey; player.load(); player.play(); }, play: function() { player.play(); }, pause: function() { player.pause(); }, stop: function(){ player.currentTime = 0; player.pause(); }, seek: function(relative){ player.currentTime = player.duration * relative; player.play(); }, getDuration: function() { return player.duration; }, getPosition: function() { return player.currentTime; }, setVolume: function(val) { if(a){ a.volume = val / 100; } } }; }; var flashDriver = function() { var engineId = 'scPlayerEngine', player, flashHtml = function(url) { var swf = '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.permalink_url; 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) { $.getJSON(scApiUrl(link.url, apiKey), function(data) { // log('data loaded', link.url, data); index += 1; 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.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.scPlayer', playerObj: playerObj}); } }); }; // 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.scPlayer', [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('sc-player').id]; }, updatePlayStatus = function(player, status) { if(status){ // reset all other players playing status $('div.sc-player.playing').removeClass('playing'); } $(player) .toggleClass('playing', status) .trigger((status ? 'onPlayerPlay' : 'onPlayerPause') + '.scPlayer'); }, 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('.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(); // continue playing through all players // TODO create a nicer auto-play flow 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.sc-player:first').find('.sc-trackslist li.active'); } $nextItem.click(); }, onSeek = function(player, relative) { audioEngine.seek(relative); }, soundVolume = function() { var vol = 80, cooks = document.cookie.split(';'), volRx = new RegExp('scPlayer_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 = ['scPlayer_volume=', vol, '; expires=', date.toUTCString(), '; path="/"'].join(''); // update the volume in the engine audioEngine.setVolume(soundVolume); }, positionPoll; // listen to audio engine events $doc .bind('scPlayer:onAudioReady', function(event) { log('onPlayerReady: audio engine is ready'); audioEngine.play(); // set initial volume onVolume(soundVolume); }) // when the loaded track started to play .bind('scPlayer: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.scPlayer', duration: duration, position: position, relative: relative }); }, 500); }) // when the loaded track is was paused .bind('scPlayer:onMediaPause', function(event) { clearInterval(positionPoll); positionPoll = null; }) // change the volume .bind('scPlayer:onVolumeChange', function(event) { onVolume(event.volume); }) .bind('scPlayer:onMediaEnd', function(event) { onFinish(); }) .bind('scPlayer:onMediaBuffering', function(event) { updates.$buffer.css('width', event.percent + '%'); }); // Generate custom skinnable HTML/CSS/JavaScript based SoundCloud players from links to SoundCloud resources $.scPlayer = function(options, node) { var opts = $.extend({}, $.scPlayer.defaults, options), playerId = players.length, $source = node && $(node), sourceClasses = $source[0].className.replace('sc-player', ''), links = opts.links || $.map($('a', $source).add($source.filter('a')), function(val) { return {url: val.href, title: val.innerHTML}; }), $player = $('
').data('sc-player', {id: playerId}), $artworks = $('
    ').appendTo($player), $info = $('

    X
    ').appendTo($player), $controls = $('
    ').appendTo($player), $list = $('
      ').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); } // adding controls to the player $player .find('.sc-controls') .append('Play ') .end() .append('Info') .append('
      ') .find('.sc-scrubber') .append('
      ') .append('
      ') .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.scPlayer', function(event) { // log('onTrackDataLoaded.scPlayer', event.playerObj, playerId, event.target); var tracks = event.playerObj.tracks; if (opts.randomize) { tracks = shuffle(tracks); } // create the playlist $.each(tracks, function(index, track) { var active = index === 0; // create an item in the playlist $('
    1. ' + track.title + '' + timecode(track.duration) + '
    2. ').data('sc-track', {id:index}).toggleClass('active', active).appendTo($list); // create an item in the artwork list $('
    3. ') .append(artworkImage(track, index >= opts.loadArtworks)) .appendTo($artworks) .toggleClass('active', active) .data('sc-track', track); }); $player .removeClass('loading') .trigger('onPlayerInit.scPlayer'); // update the element before rendering it in the DOM $player.each(function() { if($.isFunction(opts.beforeRender)){ opts.beforeRender.call(this, tracks); } }); // set the first track's duration $('.sc-duration', $player)[0].innerHTML = timecode(tracks[0].duration); $('.sc-position', $player)[0].innerHTML = timecode(0); // set up the first track info updateTrackInfo($player, tracks[0]); // 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 $.scPlayer.stopAll = function() { $('.sc-player.playing a.sc-pause').click(); }; // plugin wrapper $.fn.scPlayer = function(options) { // reset the auto play didAutoPlay = false; // create the players this.each(function() { $.scPlayer(options, this); }); return this; }; // default plugin options $.scPlayer.defaults = $.fn.scPlayer.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.sc-player, div.sc-player').scPlayer(); }, autoPlay: false, 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: 'htuiRd1JP11Ww0X72T1C3g' }; // the GUI event bindings //-------------------------------------------------------- // toggling play/pause $('a.sc-play, a.sc-pause').live('click', function(event) { var $list = $(this).closest('.sc-player').find('ol.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('.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('.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('.sc-player'), relative = Math.min($buffer.width(), (xPos - $available.offset().left)) / $available.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: 'scPlayer:onVolumeChange', volume: getVolume(event.pageX)}); }; $node.bind('mousemove.sc-player', update); update(startEvent); }; var stopVolumeTracking = function(node, event) { $(node).unbind('mousemove.sc-player'); }; $('.sc-volume-slider') .live('mousedown', function(event) { startVolumeTracking(this, event); }) .live('mouseup', function(event) { stopVolumeTracking(this, event); }); $doc.bind('scPlayer:onVolumeChange', function(event) { $('span.sc-volume-status').css({width: event.volume + '%'}); }); // ------------------------------------------------------------------- // the default Auto-Initialization $(function() { if($.isFunction($.scPlayer.defaults.onDomReady)){ $.scPlayer.defaults.onDomReady(); } }); })(jQuery);