1 /*********************************************************************************
  2  *
  3  *   Copyright (c) 2012, American Public Media Group
  4  *
  5  *   All rights reserved.
  6  *
  7  *   Redistribution and use in source and binary forms, with or without
  8  *   modification, are permitted provided that the following conditions are met:
  9  *
 10  *   - Redistributions of source code must retain the above copyright notice,
 11  *     this list of conditions and the following disclaimer.
 12  *
 13  *   - Redistributions in binary form must reproduce the above copyright notice,
 14  *     this list of conditions and the following disclaimer in the documentation
 15  *     and/or other materials provided with the distribution.
 16  *
 17  *   - Neither the name of the American Public Media Group nor the names of
 18  *     its contributors may be used to endorse or promote products derived from
 19  *     this software without specific prior written permission.
 20  *
 21  *   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 22  *   AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 23  *   IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 24  *   ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 25  *   LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
 26  *   OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 27  *   SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 28  *   INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 29  *   CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 30  *   ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 31  *   POSSIBILITY OF SUCH DAMAGE.
 32  *
 33  **********************************************************************************/
 34 /**
 35  * @name apmplayer_ui.jquery.js
 36  * @fileOverview
 37  * @description main user-interface integration between jQuery and APMPlayer.
 38  * contains first the main object supporting integration w/ APMPlayers
 39  * and second the $.fn.apmplayer_ui definition
 40  */
 41 (function ($) {
 42 
 43     'use strict';
 44 
 45     /**
 46      * APMPlayerUI
 47      * User-interface integration w/ APMPlayer that supports $.fn.apmplayer_ui
 48      * Current version of $.fn.apmplayer_ui only allows a single instance of the UI at a time
 49      */
 50     var APMPlayerUI = function (jq_element, jq_args) {
 51 
 52         var player_ui = this;
 53 
 54         this.parent_id = '#' + jq_element.attr('id');
 55         this.args = jq_args;
 56 
 57         this.init = function () {
 58             player_ui.main.init();
 59             player_ui.controls.init();
 60             player_ui.events.init();
 61             player_ui.playlist.init();
 62         };
 63 
 64         this.main = {
 65             settings : {
 66                 autoplay      : false,   //defaults, can be overridden on player init()
 67                 muted         : false,
 68                 fetchMetadata : false,
 69                 volume        : 0.9
 70             },
 71             isReady : false,
 72             init : function () {
 73                 if (player_ui.args.hasOwnProperty('settings')) {
 74                     player_ui.main.mergeSettings(player_ui.args.settings);
 75                 }
 76                 if (player_ui.main.settings.autoplay === true
 77                         && player_ui.main.settings.fetchMetadata === true) {
 78                     player_ui.main.settings.autoplay = 'wait';  //this resolves the race-condition to make sure fetchMetadata call completes before autoplay occurs.
 79                 }
 80             },
 81             mergeSettings : function (settings) {
 82                 var prop;
 83                 for (prop in settings) {
 84                     if (settings.hasOwnProperty(prop)
 85                             && player_ui.main.settings.hasOwnProperty(prop)) {
 86                         player_ui.main.settings[prop] = settings[prop];
 87                     }
 88                 }
 89             },
 90             canAutoPlay : function () {
 91                 if (player_ui.main.settings.autoplay === true
 92                         && APMPlayer.mechanism.getCurrentSolution() !== APMPlayer.mechanism.type.HTML5
 93                         && player_ui.main.isReady === true) {
 94                     return true;
 95                 }
 96                 return false;
 97             },
 98             updateAutoPlay : function (playable) {
 99                 var current_playable = player_ui.playlist.current();
100                 if (current_playable.identifier === playable.identifier
101                         && player_ui.main.settings.autoplay === 'wait') {
102                     player_ui.main.settings.autoplay = true;
103                 }
104             },
105             fetchMetadata : function (playable) {
106                 //something like this is used by APMG to get metadata, duration, other info about a playable before it starts playing.
107                 //for this version, it simply by-passes the API call and passes through to the callback directly
108 
109                 player_ui.events.onFetchMetadata(playable);
110 
111                 //example similar to the API call used by APMG:
112                 //$.getJSON('http://mywebservice.org/api/audio/metadata/?callback=?', 'id=' + playable.identifier, player_ui.events.onFetchMetadata);
113             }
114         };
115 
116         this.skin = {
117             /**
118              * holds map of css
119              * (potentially overridable down the road if differing id's/class names are desired)
120              */
121             css : {
122                 play: "apm_player_play",
123                 pause: "apm_player_pause",
124                 seeker: "apm_player_bar",
125                 seekerBufferingCls: "buffering",
126                 seekerLoading: "apm_player_loading",
127                 liveStreamingCls: "streaming",
128                 volumeWrapper: "apm_player_volume_wrapper",
129                 volumeMutedCls: "muted",
130                 volumeBar: "apm_volume_bar",
131                 volumeBarWrapper: "apm_player_volume_slider_wrapper",
132                 volumeStatus: "apm_player_volume_status",
133                 info: "apm_player_info",
134                 status: "apm_player_status",
135                 statusWarningCls: "warning",
136                 statusAlertCls: "alert",
137                 playtime: "apm_player_playtime",
138                 playlist: "apm_playlist",
139                 playlistNowPlayingCls: "nowplaying",
140                 sponsorOverlayActiveCls: "preroll-active",
141                 sponsorOverlayInactiveCls: "preroll-inactive",
142                 sponsorTimer: "apm_sponsor_overlay_time",
143                 sharingTools: "apm_sharing_tools",
144                 sharingTabControls: "apm_sharing_tab_controls",
145                 sharingTabCls: "apm_sharing_tab",
146                 sharingTabSharing: "apm_sharing_share",
147                 sharingTabDownload: "apm_sharing_download",
148                 sharingTabEmbed: "apm_sharing_embed",
149                 sharingTabLink: "apm_sharing_link"
150             }
151         };
152 
153         this.controls = {
154             /**
155              * registers jQuery events, listens for interactions w/ skin + inits jQuery sliders, etc
156              */
157             init : function () {
158                 player_ui.controls.seeker.init();
159                 player_ui.controls.info.init();
160                 player_ui.controls.volume.init();
161                 player_ui.controls.volumeStatus.init();
162                 player_ui.controls.pause.init();
163                 player_ui.controls.play.init();
164                 player_ui.controls.tools.init();
165             },
166             play : {
167                 init : function () {
168                     $(player_ui.parent_id + ' #' + player_ui.skin.css.play).click(function () {
169                         APMPlayer.play(player_ui.playlist.current(), player_ui.main.settings);
170                     });
171                 }
172             },
173             pause : {
174                 init : function () {
175                     $(player_ui.parent_id + ' #' + player_ui.skin.css.pause).click(function () {
176                         APMPlayer.pause();   // note, this will force an unload of playable in APMPlayer internally
177                     });                      // if it's type === LIVE_AUDIO (conserves BW)
178                 }
179             },
180             seeker : {
181                 status : 'NORMAL',  //or USER_SLIDING
182                 init : function () {
183                     $(player_ui.parent_id + ' #' + player_ui.skin.css.seeker).slider({
184                         disabled: true,
185                         range: "min",
186                         start: function (event, ui) {
187                             player_ui.controls.seeker.status = 'USER_SLIDING';
188                         },
189                         stop: function (event, ui) {
190                             player_ui.events.onSeek(ui.value / 100);
191                             player_ui.controls.seeker.status = 'NORMAL';
192                         },
193                         slide: function (event, ui) {
194                             player_ui.controls.seeker.status = 'USER_SLIDING';
195                             var current_playable = player_ui.playlist.current();
196                             current_playable.position = (ui.value / 100) * current_playable.duration;
197                             player_ui.controls.playtime.render(current_playable);
198                         }
199                     });
200                 },
201                 update : function (playable) {
202                     if (player_ui.controls.seeker.status === 'NORMAL') {   //don't slide if user_sliding
203                         var percent = 100 * playable.percent_played;
204                         $(player_ui.parent_id + ' #' + player_ui.skin.css.seeker).slider('value', percent);
205                     }
206                     if(playable.percent_loaded <= 1) {
207                         var percent_num = 100 * (playable.percent_loaded);
208                         $(player_ui.parent_id + ' #' + player_ui.skin.css.seekerLoading).width(percent_num + '%');
209                     }
210                 },
211                 enable : function () {
212                     if (player_ui.playlist.current().type !== APMPlayer.mediaTypes.LIVE_AUDIO) {
213                         $(player_ui.parent_id + ' #' + player_ui.skin.css.seeker).slider('enable');
214                     }
215                 },
216                 disable : function () {
217                     if (player_ui.playlist.current().type === APMPlayer.mediaTypes.LIVE_AUDIO) {
218                         $(player_ui.parent_id + ' #' + player_ui.skin.css.seeker).slider('disable');
219                     }
220                 },
221                 reset : function () {
222                     $(player_ui.parent_id + ' #' + player_ui.skin.css.seeker).slider('value', 0);
223                 },
224                 configure : function (playable) {
225                     player_ui.controls.seeker.reset();
226                     if(playable.type === APMPlayer.mediaTypes.LIVE_AUDIO) {
227                         player_ui.controls.seeker.disable();
228                         $(player_ui.parent_id + ' #' + player_ui.skin.css.seeker).addClass(player_ui.skin.css.liveStreamingCls);
229                     } else {
230                         if(playable.duration > 0) {
231                             player_ui.controls.seeker.enable();
232                         }
233                         $(player_ui.parent_id + ' #' + player_ui.skin.css.seeker).removeClass(player_ui.skin.css.liveStreamingCls);
234                     }
235                 }
236             },
237             playtime : {
238                 convertToTime : function (miliseconds) {
239                     var myTime = new Date(miliseconds),
240                         hour = myTime.getUTCHours(),
241                         min = myTime.getUTCMinutes(),
242                         sec = myTime.getUTCSeconds(),
243                         strHour = hour,
244                         strMin = (strHour > 0 && min < 10) ? '0' + min : min,
245                         strSec = (sec < 10) ? '0' + sec : sec;
246 
247                     return ((strHour > 0) ? strHour + ':' : '') + (strMin + ':') + (strSec);
248                 },
249                 render : function (playable) {
250                     var time_display;
251                     if (playable.duration > 0) {
252                         time_display = player_ui.controls.playtime.convertToTime(playable.position) + ' / ' + player_ui.controls.playtime.convertToTime(playable.duration);
253                     } else {
254                         time_display = player_ui.controls.playtime.convertToTime(playable.position);
255                     }
256                     $(player_ui.parent_id + ' #' + player_ui.skin.css.playtime).text(time_display);
257                 }
258             },
259             volume : {
260                 init : function () {
261                     $(player_ui.parent_id + ' #' + player_ui.skin.css.volumeBar).slider({
262                         range: 'min',
263                         orientation: 'vertical',
264                         value : player_ui.main.settings.volume * 100,
265                         stop : function (event, ui) {
266                             APMPlayer.setVolume(ui.value / 100);
267                             if (ui.value > 0) {
268                                 if (player_ui.main.settings.muted) {
269                                     APMPlayer.unmute();
270                                     player_ui.controls.volumeStatus.renderUnmuted();
271                                     player_ui.main.settings.muted = false;
272                                 }
273                             }
274                         }
275                     });
276 
277                     $(player_ui.parent_id + ' #' + player_ui.skin.css.volumeWrapper).hover(function(){
278                         $(player_ui.parent_id + ' #' + player_ui.skin.css.volumeBarWrapper).show();
279                     },
280                     function(){
281                         $(player_ui.parent_id + ' #' + player_ui.skin.css.volumeBarWrapper).fadeOut(500);
282                     });
283                 },
284                 renderMuted : function () {
285                     $(player_ui.parent_id + ' #' + player_ui.skin.css.volumeBar).slider('value', 0);
286                 },
287                 renderUnmuted : function () {
288                     $(player_ui.parent_id + ' #' + player_ui.skin.css.volumeBar).slider('value', player_ui.main.settings.volume * 100);
289                 }
290             },
291             volumeStatus : {
292                 init : function () {
293                     $(player_ui.parent_id + ' #' + player_ui.skin.css.volumeStatus).click(function () {
294                         if (player_ui.main.settings.muted) {
295                             APMPlayer.unmute();
296                             player_ui.main.settings.muted = false;
297                             player_ui.controls.volume.renderUnmuted();
298                             player_ui.controls.volumeStatus.renderUnmuted();
299                         } else {
300                             APMPlayer.mute();
301                             player_ui.main.settings.muted = true;
302                             player_ui.controls.volume.renderMuted();
303                             player_ui.controls.volumeStatus.renderMuted();
304                         }
305                     });
306                 },
307                 renderMuted : function () {
308                     $(player_ui.parent_id + ' #' + player_ui.skin.css.volumeStatus).addClass(player_ui.skin.css.volumeMutedCls);
309                 },
310                 renderUnmuted : function () {
311                     $(player_ui.parent_id + ' #' + player_ui.skin.css.volumeStatus).removeClass(player_ui.skin.css.volumeMutedCls);
312                 }
313             },
314             info : {
315                 init : function () {
316                     if (player_ui.args.hasOwnProperty('onMetadata')) {   //bind custom event handler, if passed through $.args
317                         player_ui.events.onMetadata = player_ui.args.onMetadata;
318                     }
319                 }
320             },
321             status : {
322                 displayWarning : function (html_snippet) {
323                     $(player_ui.parent_id + ' #' + player_ui.skin.css.status).html(html_snippet);
324                     $(player_ui.parent_id + ' #' + player_ui.skin.css.status).addClass(player_ui.skin.css.statusWarningCls);
325                 },
326                 displayAlert : function (html_snippet) {
327                     $(player_ui.parent_id + ' #' + player_ui.skin.css.status).html(html_snippet);
328                     $(player_ui.parent_id + ' #' + player_ui.skin.css.status).addClass(player_ui.skin.css.statusAlertCls);
329                 },
330                 clearAll : function () {
331                     $(player_ui.parent_id + ' #' + player_ui.skin.css.status).removeClass(player_ui.skin.css.statusAlertCls);
332                     $(player_ui.parent_id + ' #' + player_ui.skin.css.status).removeClass(player_ui.skin.css.statusWarningCls);
333                     $(player_ui.parent_id + ' #' + player_ui.skin.css.status).html('');
334                 }
335             },
336             tools : {
337                 init : function () {
338                     if (player_ui.args.hasOwnProperty('tools')
339                             && player_ui.args.tools.hasOwnProperty('config')) {
340                         player_ui.controls.tools.config = player_ui.args.tools.config;
341                     }
342 
343                     player_ui.controls.tools.config();
344                 },
345                 config : function () {
346                     $(player_ui.parent_id + ' #' + player_ui.skin.css.sharingTools + ' .' + player_ui.skin.css.sharingTabCls).hide();
347                     $(player_ui.parent_id + ' #' + player_ui.skin.css.sharingTools + ' .' + player_ui.skin.css.sharingTabCls + ':first').show();
348                     $(player_ui.parent_id + ' #' + player_ui.skin.css.sharingTools + ' ul#' + player_ui.skin.css.sharingTabControls + ' li:first').addClass('active');
349                     $(player_ui.parent_id + ' #' + player_ui.skin.css.sharingTools + ' ul#' + player_ui.skin.css.sharingTabControls + ' li a').click(function() {
350                         $(player_ui.parent_id + ' #' + player_ui.skin.css.sharingTools + ' ul#' + player_ui.skin.css.sharingTabControls + ' li').removeClass('active');
351                         $(this).parent().addClass('active');
352                         var currentTab = $(this).attr('href');
353                         $(player_ui.parent_id + ' #' + player_ui.skin.css.sharingTools + ' .' + player_ui.skin.css.sharingTabCls).hide();
354                         $(currentTab).show();
355 
356                         return false;
357                     });
358                 },
359                 renderDownload : function (playable) {
360                     var snippet = '';
361                     if(playable.downloadable === true
362                             && player_ui.playlist.current().type === APMPlayer.mediaTypes.AUDIO) {
363                         snippet = '<a href="' + APMPlayer.base_path + 'util/download.php?uri=' + playable.http_file_path +'">file download</a>';
364                     } else {
365                         snippet = 'sorry, this item is not downloadable.';
366                     }
367 
368                     $(player_ui.parent_id + ' #' + player_ui.skin.css.sharingTools + ' #' + player_ui.skin.css.sharingTabDownload).html(snippet);
369                 }
370             }
371         };
372 
373         /**
374          * player_ui.events
375          * contains all handlers + registers listeners for UI and back-end library
376          * essentially the brokers communication as a controller between display and playback mechanism
377          */
378         this.events = {
379             init : function () {
380                 APMPlayer.events.addListener(APMPlayer.events.type.PLAYER_READY, player_ui.events.onPlayerReady);
381                 APMPlayer.events.addListener(APMPlayer.events.type.PLAYING, player_ui.events.onPlaying);
382                 APMPlayer.events.addListener(APMPlayer.events.type.PAUSED, player_ui.events.onPaused);
383                 APMPlayer.events.addListener(APMPlayer.events.type.METADATA, player_ui.events.onMetadata);
384                 APMPlayer.events.addListener(APMPlayer.events.type.BUFFER_START, player_ui.events.onBufferStart);
385                 APMPlayer.events.addListener(APMPlayer.events.type.BUFFER_END, player_ui.events.onBufferEnd);
386                 APMPlayer.events.addListener(APMPlayer.events.type.POSITION_UPDATE, player_ui.events.onPositionUpdate);
387                 APMPlayer.events.addListener(APMPlayer.events.type.FINISHED, player_ui.events.onFinished);
388                 APMPlayer.events.addListener(APMPlayer.events.type.UNLOADED, player_ui.events.onUnloaded);
389                 APMPlayer.events.addListener(APMPlayer.events.type.VOLUME_UPDATED, player_ui.events.onVolumeUpdated);
390                 APMPlayer.events.addListener(APMPlayer.events.type.CONNECTION_LOST, player_ui.events.onConnectionLost);
391                 APMPlayer.events.addListener(APMPlayer.events.type.PLAYER_FAILURE, player_ui.events.onFailure);
392                 APMPlayer.events.addListener(APMPlayer.events.type.MISSING_FILE, player_ui.events.onMissingFile);
393             },
394             onPlayerReady : function () {
395                 player_ui.main.isReady = true;
396                 if (player_ui.main.canAutoPlay()) {
397                     APMPlayer.play(player_ui.playlist.current(), player_ui.main.settings);
398                 } else {
399                     var current_playable = player_ui.playlist.current();
400                     if(current_playable !== null) {
401                         player_ui.events.onMetadata(current_playable);
402                     }
403                 }
404             },
405             onPlaying : function (playable) {
406                 player_ui.controls.seeker.enable();
407                 player_ui.controls.status.clearAll();
408                 $(player_ui.parent_id + ' #' + player_ui.skin.css.play).hide();
409                 $(player_ui.parent_id + ' #' + player_ui.skin.css.pause).show();
410 
411                 player_ui.playlist.addNowPlaying(playable);
412             },
413             onPaused : function (playable) {
414                 $(player_ui.parent_id + ' #' + player_ui.skin.css.play).show();
415                 $(player_ui.parent_id + ' #' + player_ui.skin.css.pause).hide();
416 
417                 if (playable.type === APMPlayer.mediaTypes.LIVE_AUDIO) {
418                     player_ui.controls.seeker.reset();
419                 }
420             },
421             onFinished : function () {
422                 player_ui.controls.seeker.disable();
423                 if (player_ui.playlist.hasNext()) {
424                     player_ui.playlist.next();
425                 } else {
426                     APMPlayer.unload();
427                 }
428             },
429             onPositionUpdate : function (playable) {
430                 player_ui.controls.seeker.update(playable);
431 
432                 if(player_ui.controls.seeker.status !== 'USER_SLIDING') {
433                     player_ui.controls.playtime.render(playable);
434                 }
435             },
436             onSeek : function (percent_decimal) {
437                 if (APMPlayer.state.current() !== APMPlayer.state.type.STOPPED) {
438                     APMPlayer.seek(percent_decimal);
439                 } else {
440                     var current_playable = player_ui.playlist.current();  //manually set if stopped
441                     if (current_playable.duration > 0) {
442                         current_playable.percent_played = percent_decimal;
443                         current_playable.position = current_playable.duration * percent_decimal;
444                         player_ui.controls.playtime.render(current_playable);
445                     }
446                 }
447             },
448             onUnloaded : function (playable) {
449                 player_ui.controls.seeker.reset();
450                 player_ui.controls.playtime.render(playable);
451                 $(player_ui.parent_id + ' #' + player_ui.skin.css.play).show();
452                 $(player_ui.parent_id + ' #' + player_ui.skin.css.pause).hide();
453             },
454             onBufferStart : function () {
455                 if(APMPlayer.state.current() === APMPlayer.state.type.PLAYING) {
456                     $(player_ui.parent_id + ' #' + player_ui.skin.css.seeker).addClass(player_ui.skin.css.seekerBufferingCls);
457                 }
458             },
459             onBufferEnd : function () {
460                 $(player_ui.parent_id + ' #' + player_ui.skin.css.seeker).removeClass(player_ui.skin.css.seekerBufferingCls);
461             },
462             onFetchMetadata : function (response) {
463                 //APMG-specific metadata API callback would normally appear here.
464                 //instead, this is here as a place-holder so this version works if this setting is enabled.
465 
466                 APMPlayer.debug.log('events.onFetchMetadata() pass-through hit for  \'' + response.identifier + '\'', APMPlayer.debug.type.info, 'APMPlayerUI');
467 
468                 var current_playable = player_ui.playlist.current();
469                 if (response.identifier === current_playable.identifier) {
470                     player_ui.events.onMetadata(current_playable);               //set common display window
471                     player_ui.controls.tools.renderDownload(current_playable);   //make sure download link is correct
472 
473                     if (current_playable.duration > 0) {
474                         player_ui.controls.playtime.render(current_playable);    //set time
475 
476                         if(current_playable.isFlashStreamable()) {               //make scrubbable (but only if not prog-download)
477                             player_ui.controls.seeker.enable();
478 
479                             if(current_playable.position > 0) {                  //in this situation, the current position was defaulted to start somewhere mid-track.  be sure percent_played is set appropriately.
480                                 current_playable.percent_played = current_playable.position / current_playable.duration;
481                             }
482                             player_ui.controls.seeker.update(current_playable);
483                         }
484                     }
485 
486                     player_ui.main.updateAutoPlay(current_playable);
487                     if (player_ui.main.canAutoPlay()) {
488                         APMPlayer.play(player_ui.playlist.current(), player_ui.main.settings);
489                     }
490                 }
491             },
492             onMetadata : function () {},  //typically overridden on fn.init()
493             onFailure : function () {
494                 var playable = player_ui.playlist.current();
495                 var snippet = '<p>We\'re sorry, but your browser is not able to play the stream.  Please try one of these options:  <br /><br />1) Install or enable <a href="http://get.adobe.com/flashplayer/" target="_blank">Adobe Flash Player</a> <br />2) Use a browser that supports HTML5 and MP3 audio, such as <a href="http://www.google.com/chrome/" target="_blank">Chrome</a>, <a href="http://www.apple.com/safari/download/" target="_blank">Safari</a>, or <a href="http://www.microsoft.com/ie9" target="_blank">Internet Explorer 9</a>';
496 
497                 //strange-case.  if we're dealing with Safari on Windows7, quicktime is required for HTML5  (yuck!)  this catches that condition.
498                 if (/Safari/.test(navigator.userAgent) && /Apple Computer/.test(navigator.vendor)) {
499                     snippet = '<p>We\'re sorry, but your browser is not able to play the stream.  Please try one of these options:  <br /><br />1) Install or enable <a href="http://get.adobe.com/flashplayer/" target="_blank">Adobe Flash Player</a> <br />2) Install <a href="http://www.apple.com/quicktime/download/" target="_blank">Quicktime</a> for HTML5 support for Safari on Windows (requires reboot) <br />2b) Use a different browser that natively supports HTML5 and MP3 audio, such as <a href="http://www.google.com/chrome/" target="_blank">Chrome</a> or <a href="http://www.microsoft.com/ie9" target="_blank">Internet Explorer 9</a>';
500                 }
501 
502                 if (playable.downloadable === true
503                         && playable.http_file_path !== '') {
504 
505                     if (playable.type === APMPlayer.mediaTypes.AUDIO) {
506                         snippet += '<br />3) <a href="' + APMPlayer.base_path + 'util/download.php?uri=' + playable.http_file_path + '">download the audio</a>';
507                     } else if (playable.type === APMPlayer.mediaTypes.LIVE_AUDIO) {
508                         snippet += '<br />3) <a href="' + playable.http_file_path + '">Stream the audio using a third-party player (eg. iTunes)</a>';
509                     }
510                 }
511                 snippet += "</p>";
512 
513                 player_ui.controls.status.displayWarning(snippet);
514             },
515             onVolumeUpdated : function (eventData) {
516                 player_ui.main.settings.volume = eventData.percent_decimal;
517             },
518             onConnectionLost : function () {
519                 APMPlayer.unload();
520                 var snippet = "<p>Your network connection has changed or has been lost.<br /><br />Please check your connection, then click play to resume.";
521                 player_ui.controls.status.displayAlert(snippet);
522             },
523             onMissingFile : function (playable) {
524                 //display error message for missing file.
525                 //unfortunately, most missing file errors are actually a problem w/ a network connection and rarely are actual missing files.
526                 //the error output has been adjusted to reflect and offer better feedback.
527                 var snippet = '<p>We\'re sorry, an error has occurred and your audio cannot be played at this time. <br /><br />Often, this error is a result of a poor or missing internet connection.  Please check your internet connection, then click play to resume.';
528                 player_ui.controls.status.displayWarning(snippet);
529                 player_ui.underwriting.removeOverlay();
530             }
531         };
532 
533         /**
534          * playlist
535          * note, UI will always maintain an internal playist, regardless of appearance of playlist.
536          * used internally, even if only one playable.
537          */
538         this.playlist = APMPlayerFactory.getPlaylist();
539         player_ui.playlist.onUpdate = function (playable) {};
540         player_ui.playlist.init = function () {
541             if (player_ui.args.hasOwnProperty('onPlaylistUpdate')) {
542                 player_ui.playlist.onUpdate = player_ui.args.onPlaylistUpdate;
543             }
544             if (player_ui.args.hasOwnProperty('playables')) {
545                 $.each(player_ui.args.playables, function (index, item) {
546                     player_ui.playlist.addPlayable(item);
547                 });
548             }
549         };
550         player_ui.playlist.addPlayable = function (item) {
551             var playable = APMPlayerFactory.getPlayable(item);
552             if (playable.isValid()) {
553                 player_ui.playlist.add(playable);
554                 player_ui.playlist.onUpdate(playable);
555 
556                 if (player_ui.main.settings.fetchMetadata === true) {
557                     if (playable.isCustomScheme('apm_audio')) {               //only look-up custom APM media items
558                         player_ui.main.fetchMetadata(playable);
559                     } else {
560                         player_ui.main.updateAutoPlay(playable);
561                     }
562                 }
563 
564             } else {
565                 APMPlayer.debug.log('sorry, there was a problem with the parameters passed and a valid playable could not be created.', APMPlayer.debug.type.warn, 'APMPlayerUI');
566             }
567         };
568         player_ui.playlist.gotoItem = function (identifer) {
569             if(APMPlayer.state.current() === APMPlayer.state.type.STOPPED
570                     || player_ui.playlist.current().identifier !== identifer) {
571                 player_ui.controls.seeker.disable();
572                 player_ui.playlist.goto(identifer);
573             }
574         };
575         player_ui.playlist.addNowPlaying = function (playable) {
576             $('li[ id = \'' + playable.identifier + '\']').addClass(player_ui.skin.css.playlistNowPlayingCls);
577         };
578         player_ui.playlist.removeNowPlaying = function (playable) {
579             $(player_ui.parent_id + ' #' + player_ui.skin.css.playlist + ' li[ id = \'' + playable.identifier + '\']').removeClass(player_ui.skin.css.playlistNowPlayingCls);
580         };
581         player_ui.playlist.onCurrentChange = function (previous_playable) {
582             if (previous_playable !== null) {   //only null on first item added to playlist.
583                 player_ui.playlist.removeNowPlaying(previous_playable);
584                 APMPlayer.play(player_ui.playlist.current(), player_ui.main.settings);
585             }
586             var current_playable = player_ui.playlist.current();
587             player_ui.controls.seeker.configure(current_playable);
588             player_ui.controls.tools.renderDownload(current_playable);
589 
590             if (current_playable.duration > 0) {
591                 player_ui.controls.playtime.render(current_playable);
592             }
593         };
594         player_ui.playlist.events.addListener(APMPlayer.events.type.PLAYLIST_CURRENT_CHANGE, player_ui.playlist.onCurrentChange);
595 
596 
597         player_ui.init();    //always init on construct
598 
599 
600     };  //end APMPlayerUI
601 
602 
603     /**
604      * @name $.fn.apmplayer_ui
605      * @class
606      *
607      * @description main integration point between jQuery, APMPlayerUI, and {@link APMPlayer}.
608      * The $.fn.apmplayer_ui is tested and compliant with all versions of jQuery >= v1.4
609      *
610      * @param {Object} args object literal used to instantiate APMPlayerUI
611      * @param {Object} args.settings object literal containing initial values to set on APMPlayer initialization.
612      * @param {Number} args.settings.volume sets volume on initialization.  (eg. volume : 0.5)
613      * @param {Boolean} args.settings.autoplay autoplays current {@link Playable} in {@link Playlist} after library init completes. (default : false)
614      * @param {Boolean} args.settings.fetchMetadata retrieves and sets additional metadata for each {@link Playable}.  Note that this will only override a value in {@link Playable} if it is currently not set.  Values passed in when adding a {@link Playable} will take precedence.  (default : true)
615      * @param {Array} args.playables an array of {@link Playable} objects.
616      * @param {Function} args.onPlaylistUpdate() a method that will fire when Events.type.PLAYLIST_CURRENT_CHANGE is fired, or the playlist is updated. (see {@link Events}).
617      * @param {Function} args.onMetadata() a method that will fire when Events.type.METADATA is fired. (see {@link Events}).
618      *
619      * @example
620      * //note: see CustomSchemes for this example
621      * $('#apm_media_wrapper').apmplayer_ui({
622      *    playables : [
623      *        {
624      *            identifier: 'apm_audio:/performance_today/2012/04/24/pt2_20120424_128.mp3'
625      *        }
626      *    ]
627      * });
628      *
629      * @example
630      * $('#apm_media_wrapper').apmplayer_ui({
631      *    playables : [
632      *        {
633      *           identifier: 'my audio',
634      *           type: 'audio',
635      *           description: 'more info about my audio',
636      *           flash_server_url: 'rtmp://server/',
637      *           flash_file_path: 'mp3:path/file.mp3',
638      *           http_file_path: 'http://server/file.mp3'
639      *        }
640      *    ]
641      * });
642      *
643      *
644      * @example
645      * $('#apm_media_wrapper').apmplayer_ui({
646      *    settings : {
647      *        volume : 0.8,
648      *        autoplay : true
649      *    },
650      *    playables : [
651      *       {
652      *          identifier: 'apm_live_audio:/mpr_current',
653      *          description: 'live streaming from 89.3',
654      *          program: '89.3 the Current',
655      *          host: 'Mark Wheat',
656      *          date: 'March 24, 2012',
657      *          detail: 'during this hour...',
658      *          image_sm: 'http://mpr.org/images/current.gif'
659      *       },
660      *       {
661      *           identifier: 'apm_audio:/performance_today/2012/04/24/pt2_20120424_128.mp3',
662      *           program: 'on Being',
663      *           downloadable: false
664      *       }
665      *    ],
666      *    onPlaylistUpdate : function (playable) {
667      *       // implement
668      *    },
669      *    onMetadata : function (playable) {
670      *       // implement
671      *    }
672      * });
673      */
674     $.fn.apmplayer_ui = function (args) {
675 
676         //check dependencies
677         if (typeof APMPlayer === 'undefined'
678                 || typeof soundManager === 'undefined') {
679             $.error('apmplayer_ui ERROR.  1 or more dependent libraries missing.  exiting.');
680             return null;
681         }
682 
683         var jq_element = this,  //hold reference to jQuery $(this)
684             methods = {
685                 /**
686                  * @name addPlayable
687                  *
688                  * @description adds a {@link Playable}, if valid, to {@link Playlist}.
689                  * @example $('#player_container_div').apmplayer_ui('addPlayable', playable);
690                  * @methodOf $.fn.apmplayer_ui
691                  */
692                 addPlayable : function (item) {
693                     if (typeof window.apmplayer_ui !== 'undefined') {
694                         window.apmplayer_ui.playlist.addPlayable(item);
695                     } else {
696                         APMPlayer.debug.log('you must first initialize apmplayer_ui before calling methods on it.', APMPlayer.debug.type.error, 'APMPlayerUI');
697                     }
698                 },
699                 /**
700                  * @name gotoPlaylistItem
701                  *
702                  * @description changes position in {@link Playlist} to the {@link Playable} that matches the passed identifier, and begins playing the item.
703                  * @example $('#player_container_div').apmplayer_ui('gotoPlaylistItem', identifier);
704                  * @methodOf $.fn.apmplayer_ui
705                  */
706                 gotoPlaylistItem : function (identifier) {
707                     if (typeof window.apmplayer_ui !== 'undefined') {
708                         window.apmplayer_ui.playlist.gotoItem(identifier);
709                     } else {
710                         APMPlayer.debug.log('you must first initialize apmplayer_ui before calling methods on it.', APMPlayer.debug.type.error, 'APMPlayerUI');
711                     }
712                 }
713             };
714 
715         if (methods[args]) {
716             return methods[args].apply(this, Array.prototype.slice.call(arguments, 1));
717         } else if (typeof args === 'object' || !args) {
718             if (typeof window.apmplayer_ui === 'undefined') {
719                 window.apmplayer_ui = new APMPlayerUI(jq_element, args);
720                 APMPlayer.debug.log('instantiated apmplayer_ui', APMPlayer.debug.type.info, 'APMPlayerUI');
721             } else {
722                 APMPlayer.debug.log('sorry, only one player UI instance is currently supported.', APMPlayer.debug.type.error, 'APMPlayerUI');
723             }
724         } else {
725             APMPlayer.debug.log('Method ' +  args + ' does not exist on jQuery.apmplayer_ui', APMPlayer.debug.type.error, 'APMPlayerUI');
726         }
727     };
728 }(jQuery));
729