1/* 2* SoundCloud Custom Player jQuery Plugin 3* Author: Matas Petrikas, [email protected] 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 +'&enable_api=true&player_type=engine&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 + '">
175'+ 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">
526' + 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);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.