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.js 36 * @fileOverview 37 * @description the file contains APMPlayerFactory, APMPlayer, Playlist, Playable etc. 38 * All methods are revealed via API through the use of 'revealing' or 'module' js pattern. 39 */ 40 if (typeof APMPlayerFactory === 'undefined') { 41 42 /** 43 * @name APMPlayerFactory 44 * @description factory pattern used to prohibit multiple copies of APMPlayer (singleton) + provides access to Playlist and Playable creation 45 * @class 46 */ 47 var APMPlayerFactory = (function () { 48 49 'use strict'; 50 51 /** 52 * @name MediaTypes 53 * @description contains the names of all valid Media Types that APMPlayer handles -- also, see {@link Playable}. 54 * @class 55 * @ignore 56 */ 57 var MediaTypes = { 58 type : { 59 AUDIO : 'audio', 60 LIVE_AUDIO : 'live_audio' 61 //VIDEO : 'video', 62 }, 63 isValid : function (type) { 64 var key; 65 for (key in MediaTypes.type) { 66 if (MediaTypes.type[key] === type) { 67 return true; 68 } 69 } 70 return false; 71 } 72 }; 73 74 /** 75 * @name Debug 76 * @description static debugging utility. To enable debug messages from browser session, simply add '?debug=1' or 'debug=true' to URL when loading any APMPlayer instance (enables console logging). To disable Console + onScreen debug trace, use 'debug=all'. 77 * @class 78 */ 79 var Debug = function () {}; 80 /** 81 * @name enabled 82 * @description used to mark if debugging logs should output or not. to enable, add ?debug=1 to URL when loading player. by default, it logs to the console only; to enable a debug div, do debug=all 83 * @default false 84 * @fieldOf Debug 85 */ 86 Debug.enabled = false; 87 /** 88 * @name consoleOnly 89 * @description used to mark if debugging logs should output to console only; true by default. use debug=all to log to both the console AND a helper div on screen. 90 * @default true 91 * @fieldOf Debug 92 */ 93 Debug.consoleOnly = true; 94 /** 95 * @name log 96 * @description writes to the log 97 * @param {string} message the information to log 98 * @param {string} type level of severity to log (see type) 99 * @param {string} object_name optional object name to pass (if logging outside of APMPlayer -- APMPlayer_UI for instance) 100 * 101 * @methodOf Debug 102 */ 103 Debug.log = function (message, type, object_name) { 104 if (Debug.enabled === false) { 105 return; 106 } 107 if (typeof soundManager !== 'undefined') { 108 if (typeof object_name === 'undefined') { 109 object_name = 'APMPlayer'; 110 } 111 soundManager._writeDebug(object_name + '::' + message, type.id, false); 112 } else { 113 console.log(object_name + '::' + message + '[' + type.name + ']'); 114 } 115 }; 116 /** 117 * @name type 118 * @description object that holds the three different logging levels 119 * @example 120 * Debug.type.info 121 * Debug.type.warn 122 * Debug.type.error 123 * @fieldOf Debug 124 */ 125 Debug.type = { 126 'info' : {'id' : 1, 'name' : 'info'}, 127 'warn' : {'id' : 2, 'name' : 'warning'}, 128 'error': {'id' : 3, 'name' : 'error'} 129 }; 130 131 /** 132 * @name Events 133 * @description creates a new Events object. Currently used within {@link APMPlayer}, internal and external version + {@link Playlist} 134 * @constructor 135 */ 136 var Events = function () { 137 /** 138 * @name type 139 * @description holds all valid event types -- See Event Summary for details 140 * @type Object 141 * @fieldOf Events 142 */ 143 this.type = { 144 /** 145 * @name AUDIO_LIB_READY 146 * @event 147 * @description fires after Audio library has initialized (only used internally) 148 * @example Events.type.AUDIO_LIB_READY 149 * 150 * @memberOf Events 151 */ 152 AUDIO_LIB_READY : 'AUDIO_LIB_READY', 153 /** 154 * @name MEDIA_READY 155 * @event 156 * @description fires when media has successfully loaded and is ready to be played 157 * @example Events.type.MEDIA_READY 158 * 159 * @memberOf Events 160 */ 161 MEDIA_READY : 'MEDIA_READY', 162 /** 163 * @name PLAYER_READY 164 * @event 165 * @description fires when player has completely initialized 166 * @example Events.type.PLAYER_READY 167 * @fieldOf Events 168 */ 169 PLAYER_READY : 'PLAYER_READY', 170 /** 171 * @name PLAYER_FAILURE 172 * @event 173 * @description fires when no suitable playback mechanism can be determined (final failure) 174 * @example Events.type.PLAYER_FAILURE 175 * @fieldOf Events 176 */ 177 PLAYER_FAILURE : 'PLAYER_FAILURE', 178 /** 179 * @name CONNECTION_LOST 180 * @event 181 * @description fires if the network connection is lost 182 * @example Events.type.CONNECTION_LOST 183 * @fieldOf Events 184 */ 185 CONNECTION_LOST : 'CONNECTION_LOST', 186 /** 187 * @name PLAYLIST_CURRENT_CHANGE 188 * @event 189 * @description fires when current item in playlist has updated 190 * @example Events.type.PLAYLIST_CURRENT_CHANGE 191 * @fieldOf Events 192 */ 193 MISSING_FILE : 'MISSING_FILE', 194 /** 195 * @name MISSING_FILE 196 * @event 197 * @description fires if a file that was attempted to be played is missing 198 * @example Events.type.MISSING_FILE 199 * @fieldOf Events 200 */ 201 PLAYLIST_CURRENT_CHANGE : 'PLAYLIST_CURRENT_CHANGE', 202 /** 203 * @name POSITION_UPDATE 204 * @event 205 * @description fires each time player head updates while playing-- a number in miliseconds usually accompanies the event to report current position in miliseconds. 206 * @example Events.type.POSITION_UPDATE 207 * @fieldOf Events 208 */ 209 POSITION_UPDATE : 'POSITION_UPDATE', 210 /** 211 * @name PLAYING 212 * @event 213 * @description fires when playable begins playback 214 * @example Events.type.PLAYING 215 * @fieldOf Events 216 */ 217 PLAYING : 'PLAYING', 218 /** 219 * @name PAUSED 220 * @event 221 * @description fires when playable is paused 222 * @example Events.type.PAUSED 223 * @fieldOf Events 224 */ 225 PAUSED : 'PAUSED', 226 /** 227 * @name FINISHED 228 * @event 229 * @description fires when playable finishes playing 230 * @example Events.type.FINISHED 231 * @fieldOf Events 232 */ 233 FINISHED : 'FINISHED', 234 /** 235 * @name UNLOADED 236 * @event 237 * @description fires when a playable is stopped or unloaded from player 238 * @example Events.type.UNLOADED 239 * @fieldOf Events 240 */ 241 UNLOADED : 'UNLOADED', 242 /** 243 * @name BUFFER_START 244 * @event 245 * @description fires when player starts buffering 246 * @example Events.type.BUFFER_START 247 * @fieldOf Events 248 */ 249 BUFFER_START : 'BUFFER_START', 250 /** 251 * @name BUFFER_END 252 * @event 253 * @description fires when player buffering ends 254 * @example Events.type.BUFFER_END 255 * @fieldOf Events 256 */ 257 BUFFER_END : 'BUFFER_END', 258 /** 259 * @name METADATA 260 * @event 261 * @description important for statistic tracking (web trends) fires each time a new playable is encountered, or when a new item begins playing in a live stream 262 * A {@link Playable} is always the data for each METADATA event 263 * @example Events.type.METADATA 264 * @fieldOf Events 265 */ 266 METADATA : 'METADATA', 267 /** 268 * @name VOLUME_UPDATED 269 * @event 270 * @description fires when volume is updated 271 * @example Events.type.VOLUME_UPDATED 272 * @fieldOf Events 273 */ 274 VOLUME_UPDATED : 'VOLUME_UPDATED' 275 }; 276 /** 277 * @name handlers 278 * @description array of event handler objects in form of { 'handler_name', function() {} }. 279 * @type Array[Object] 280 * @fieldOf Events 281 */ 282 this.handlers = []; 283 }; 284 Events.prototype = { 285 /** 286 * @name trigger 287 * @description fires all events handlers that match 'name' and passes eventArgs to each handler. 288 * @param {string} name name of event to fire 289 * @param {Object} eventArgs object literal to pass to all functon handlers 290 * 291 * @example APMPlayer.events.trigger(player.events.type.MEDIA_READY, { 'identifier' : this.ID }); 292 * 293 * @methodOf Events 294 */ 295 trigger : function (name, eventArgs) { 296 var i; 297 for (i = 0; i < this.handlers.length; i += 1) { 298 if (this.handlers[i].eventName === name) { 299 this.handlers[i].eventHandler.call(this, eventArgs); 300 } 301 } 302 }, 303 /** 304 * @name addListener 305 * @description adds an event listener 306 * @param {string} name the name of the event to listen for 307 * @param {Object} handler function to fire when event is called. 308 * 309 * @example APMPlayer.events.addListener(APMPlayer.events.type.PLAYER_READY, function() {}); 310 * @methodOf Events 311 */ 312 addListener : function (name, handler) { 313 if (typeof (name) !== 'string' || typeof (handler) !== 'function') { 314 Debug.log("Invalid parameters when creating listener with the following arguments: 'Name': " + name + ", 'Handler': " + handler, Debug.type.error); 315 } 316 this.handlers.push({ "eventName" : name, "eventHandler" : handler }); 317 }, 318 /** 319 * @name removeListeners 320 * @description clears out all listeners in this events objec 321 * 322 * @example APMPlayer.events.removeListeners(); 323 * @methodOf Events 324 */ 325 removeListeners : function () { 326 this.handlers = []; 327 } 328 }; 329 330 331 332 /** 333 * @name PlaybackState 334 * @constructor 335 * 336 * @description holds all PlaybackStates (PLAYING, STOPPED, PAUSED); 337 * 338 * @property {Object} type all possible PlaybackState types (PLAYING, STOPPED, PAUSED); 339 */ 340 var PlaybackState = function () { 341 this.type = { 342 PLAYING : 'PLAYING', 343 STOPPED : 'STOPPED', 344 PAUSED : 'PAUSED' 345 }; 346 this._current = this.type.STOPPED; //default 347 }; 348 PlaybackState.prototype = { 349 /** 350 * @name current 351 * @description gets current state (PLAYING, STOPPED, or PAUSED) 352 * 353 * @returns {string} state (PLAYING, STOPPED, or PAUSED) 354 * @public 355 * @methodOf PlaybackState 356 */ 357 current : function () { 358 return this._current; 359 }, 360 /** 361 * @name set 362 * 363 * @description sets current state + sets state in playable, if playable is passed. 364 * @param state the state to set 365 * @param playable optional {@link Playable} to update copy of state. 366 * 367 * @public 368 * @methodOf PlaybackState 369 */ 370 set : function (state, playable) { 371 this._current = state; 372 373 if (typeof (playable) === 'object' 374 && playable.hasOwnProperty('state')) { 375 playable.state = this._current; 376 } 377 } 378 }; 379 380 /** 381 * @name PlaybackMechanism 382 * @constructor 383 * 384 * @description holds an ordered-array of all possible playback mechanisms supported 385 * 386 * @property {Object} type all possible playback mechanism types (currently FLASH and HTML5) 387 * @property {Array[Object]} solutions array of playback mechanisms, ordered by priority to use as a solution. Currently FLASH is the primary playback mechanism. If the primary solution is deemed unacceptable for given platform, that first solution is removed and the next solution becomes first and primary. 388 */ 389 var PlaybackMechanism = function () { 390 this.type = { 391 FLASH : 'FLASH', 392 HTML5 : 'HTML5' 393 }; 394 this.solutions = [ this.type.FLASH, this.type.HTML5 ]; //defaults 395 }; 396 PlaybackMechanism.prototype = { 397 /** 398 * @name getCurrentSolution 399 * @description returns current primary playback mechanism 400 * 401 * @public 402 * @returns {string|null} returns null of no playback mechanisms exist 403 * @methodOf PlaybackMechanism 404 */ 405 getCurrentSolution : function () { 406 if (this.solutions.length > 0) { 407 return this.solutions[0]; 408 } 409 return null; 410 }, 411 /** 412 * @name removeCurrentSolution 413 * @description removes current primary playback solution 414 * 415 * @public 416 * @returns {boolean} success returns true for successful removal, false if nothing left to remove (no more solutions exist) 417 * @methodOf PlaybackMechanism 418 */ 419 removeCurrentSolution : function () { 420 if (this.solutions.length > 0) { 421 this.solutions.shift(); 422 return true; 423 } 424 Debug.log('PlaybackMechanism.removeCurrentSolution() no playback solutions remain to remove!', Debug.type.error); 425 return false; 426 }, 427 /** 428 * @name setSolutions 429 * @description sets an array of playback solutions 430 * 431 * @public 432 * @returns {boolean} success 433 * @methodOf PlaybackMechanism 434 */ 435 setSolutions : function (args) { 436 if (args instanceof Array) { 437 var valid_mechanisms = []; 438 var mechanism; 439 while (args.length > 0) { 440 mechanism = args.shift(); 441 if (this.isValid(mechanism)) { 442 valid_mechanisms.push(mechanism); 443 } else { 444 Debug.log('PlaybackMechanism.setSolutions() passed mechanism \'' + mechanism + '\' is invalid.', Debug.type.error); 445 } 446 } 447 this.solutions = valid_mechanisms; 448 return true; 449 } 450 451 Debug.log('PlaybackMechanism.setSolutions() argument passed is not an array!', Debug.type.error); 452 return false; 453 }, 454 /** 455 * @name isValid 456 * @description returns whether passed mechanism is a valid PlaybackMechanism 457 * @param {string} mechanism the playback mechanism to validate 458 * 459 * @public 460 * @returns {boolean} 461 * @methodOf PlaybackMechanism 462 */ 463 isValid : function (mechanism) { 464 var key; 465 for (key in this.type) { 466 if (this.type[key] === mechanism) { 467 return true; 468 } 469 } 470 return false; 471 } 472 }; 473 474 475 /** 476 * @name CustomSchemes 477 * @constructor 478 * 479 * @description allows the developer to create shortcuts or custom schemes for common patterns or common {@link Playable} objects in-use; 480 * NOTE: The main thing to understand about CustomSchemes is that any value set in the scheme_map definition will override anything passed 481 * in on {@link Playable} creation, if the identifier of the Playable happens to qualify as a registered CustomScheme. 482 */ 483 var CustomSchemes = function () { 484 this.schemes = []; 485 this.scheme_map = {}; 486 this.playable_attrs = []; 487 }; 488 CustomSchemes.prototype = { 489 /** 490 * @name init 491 * @description initializes the map of custom schemes -- when a new {@link Playable} object is created, a look-up of the identifier on the CustomSchemes object will happen to determine if the Playable matches one of the CustomSchemes. If so, all attributes defined in the scheme_map will override the {@link Playable} 492 * @param {Object} scheme_map object literal holding configuration, map of schemes-- each scheme can hold any number attributes that exist in {@link Playable} .. PLUS, the two below: 493 * @param {Object} scheme_map.flash_file_prefix prefix to be pre-pended to flash filepath when the {@link Playable} is instantiated. 494 * @param {Object} scheme_map.http_file_prefix prefix to be pre-pended to http filepath when the {@link Playable} is instantiated. 495 * 496 * @example 497 * var scheme_map = { 498 * apm_audio : { 499 * flash_server_url : 'rtmp://flash.server.org/music', 500 * flash_file_prefix : 'mp3:flashprefix', 501 * http_file_prefix : 'http://download.org', 502 * buffer_time : 3, 503 * type : 'audio' 504 * }, 505 * live_audio : { 506 * mpr_news : { 507 * flash_server_url : 'rtmp://flash.server.org/news', 508 * flash_file_path : 'news.stream', 509 * http_file_path : 'http://newsstream1.publicradio.org:80/', 510 * buffer_time : 6, 511 * type : 'live_audio' 512 * }, 513 * mpr_current : { 514 * flash_server_url : 'rtmp://flash.server.org/kcmp', 515 * flash_file_path : 'kcmp.stream', 516 * http_file_path : 'http://currentstream1.publicradio.org:80/', 517 * buffer_time : 6, 518 * type : 'live_audio' 519 * } 520 * } 521 * }; 522 * custom_schemes.init(scheme_map); 523 * 524 * ** in this example above, 525 * ** a Playable w/ identifer 'apm_audio:/marketplace/2012/04/18/morning_report.mp3' 526 * ** would translate to: 527 * 528 * var playable = { 529 * identifier : 'apm_audio:/marketplace/2012/04/18/morning_report.mp3', 530 * flash_server_url : 'rtmp://flash.server.org/music', 531 * flash_file_path : 'mp3:flashprefix/marketplace/2012/04/18/morning_report.mp3', 532 * http_file_path : 'http://download.org/marketplace/2012/04/18/morning_report.mp3', 533 * buffer_time : 3, 534 * type : 'audio', 535 * // + all other attributes in a Playable, with defaults 536 * }; 537 * 538 * while a Playable w/ identifer 'live_audio:/mpr_current' would translate to: 539 * var playable = { 540 * identifier : 'live_audio:/mpr_current', 541 * flash_server_url : 'rtmp://flash.server.org/kcmp', 542 * flash_file_path : 'kcmp.stream', 543 * http_file_path : 'http://currentstream1.publicradio.org:80/', 544 * buffer_time : 6, 545 * type : 'live_audio', 546 * // + all other attributes in a Playable, with defaults 547 * }; 548 * @public 549 * @methodOf CustomSchemes 550 */ 551 init : function (scheme_map) { 552 this.scheme_map = scheme_map; 553 this.initSchemeTypes(); 554 this.initPlayableAttrs(); 555 }, 556 initSchemeTypes : function () { 557 this.schemes = []; 558 for (var type in this.scheme_map) { 559 this.schemes.push(type); 560 } 561 }, 562 initPlayableAttrs : function () { 563 this.playable_attrs = []; 564 var playable = new Playable({}); 565 for(var propertyName in playable) { 566 if(typeof playable[propertyName] !== 'function') { 567 this.playable_attrs.push(propertyName); 568 } 569 } 570 }, 571 hasSchemes : function () { 572 if (this.schemes.length > 0) { 573 return true; 574 } 575 return false; 576 }, 577 isValid : function (scheme) { 578 if (this.schemes.indexOf(scheme) !== -1) { 579 return true; 580 } 581 return false; 582 }, 583 isScheme : function (identifier, scheme) { 584 var result = this.parse(identifier); 585 if (result !== null 586 && result.scheme === scheme) { 587 return true; 588 } 589 return false; 590 }, 591 parse : function (identifier) { 592 var pattern = '^(' + this.schemes.join('|') + '){1}(:/){1}([/\\w.-]+$)'; 593 var regex = new RegExp(pattern); 594 var result = identifier.match(regex); 595 596 if (result !== null && result.length === 4) { 597 return { 598 scheme : result[1], 599 path : result[3] 600 }; 601 } 602 return null; 603 }, 604 getValues : function (identifier) { 605 var values = {}; 606 607 var result = this.parse(identifier); 608 if (result !== null 609 && this.isValid(result.scheme)) { 610 var type_map = this.scheme_map[result.scheme]; 611 612 //check for aliases (next level down) 613 if (type_map.hasOwnProperty(result.path)) { 614 type_map = type_map[result.path]; 615 } 616 617 for (var prop in type_map) { 618 if (this.playable_attrs.indexOf(prop) !== -1) { 619 values[prop] = type_map[prop]; 620 } else if (prop === 'flash_file_prefix') { 621 values.flash_file_path = type_map[prop] + '/' + result.path; 622 } else if (prop === 'http_file_prefix') { 623 values.http_file_path = type_map[prop] + '/' + result.path; 624 } 625 } 626 } 627 return values; 628 } 629 }; 630 var custom_schemes = new CustomSchemes(); 631 632 633 /** 634 * @name Playable 635 * @class 636 * 637 * @description holds all specific information about each media item to be played in APMPlayer -- To play audio/video in APMPlayer, the creation of a valid Playable is first required. 638 * to create a playable, see {@link APMPlayerFactory.getPlayable}. Also holds complete metadata for a single playable. 639 * @param {Object} params object literal used to create a Playable object -- each of the passed object's attributes needs to match a particular item in Field Detail to be included w/ the Playable. 640 * note that the indentifier is the only required field for APM-specific audio.. However for basic playables (non-APM), type is also required. 641 * @property {string} identifier [REQUIRED] the ID for the playable (must be unique when creating a playlist). Note that this is really the only required field when creating an APM-specific Playable. 642 * @property {string} type [REQUIRED] either 'audio' or 'live_audio' -- Note that this is typically set automatically when using a {@link CustomScheme} 643 * @property {string} flash_server_url server url to connect to to stream flash (eg rtmp://archivemedia.publicradio.org/) 644 * @property {string} flash_file_path file to play over flash_server_url (eg 'mp3:filename_64.mp3') 645 * @property {string} http_file_path used for HTML5 audio playback (eg http://ondemand.publicradio.org/filename.mp3) -OR- progressive download pseudo HTML5 playback via flash. 646 * @property {string} title optional metadata field. 647 * @property {string} description optional metadata field. 648 * @property {string} detail optional metadata field. 649 * @property {string} program optional metadata field. 650 * @property {string} host optional metadata field. 651 * @property {string} date optional metadata field. 652 * @property {string} image_sm image associated w/ playable (eg used to display in info window when playable plays) -- a small and large were necessary to deal w/ re-sizing issues in IE 7,8 653 * @property {string} image_lg image associated w/ playable (eg used to display in info window when playable plays) 654 * @property {boolean} downloadable (default true) flag to permit the file to be downloaded. Shows download option and/or presents the file for download if no suitable {@link PlaybackMechanism} is present. 655 * @property {number} buffer_time amount of time (in seconds) the player should buffer -- default = 3. 656 * @property {number} duration the length, in miliseconds of this Playable 657 * @property {number} position the current position, in miliseconds for the Playable 658 * @property {number} percent_played percentage of the playable that's been played (position/duration) 659 * @property {number} percent_loaded percentage of the playable that's been loaded (percent of total duration) 660 * 661 */ 662 var Playable = function (params) { 663 var playable_self = this; 664 665 this.identifier = null; 666 this.type = null; 667 668 //playback config 669 this.flash_file_path = ''; 670 this.flash_server_url = ''; 671 this.http_file_path = ''; 672 this.buffer_time = 3; 673 this.downloadable = true; 674 675 //metadata 676 this.title = ''; 677 this.description = ''; 678 this.detail = ''; 679 this.date = ''; 680 this.program = ''; 681 this.host = ''; 682 683 //images 684 this.image_sm = ''; 685 this.image_lg = ''; 686 687 //playback statii 688 this.duration = 0; 689 this.position = 0; 690 this.percent_played = 0; 691 this.percent_loaded = 0; 692 693 //state 694 this.state = 'STOPPED'; //provided as courtesy to webTrends tracking. 695 696 //identifier is required. 697 //return empty object if not passed 698 if (typeof params.identifier === 'undefined' 699 || params.identifier === null 700 || params.identifier === '') { 701 return playable_self; 702 } 703 704 (function () { //set members 705 706 //first, set any values passed in via the playable. 707 playable_self.setMembers(params); 708 709 //second, set everything that exists in custom scheme. 710 //note that customScheme trumps all default data and strictly uses each scheme. 711 if (custom_schemes.hasSchemes()) { 712 var configParams = custom_schemes.getValues(playable_self.identifier); 713 playable_self.setMembers(configParams); 714 } 715 716 }()); 717 }; 718 Playable.prototype = { 719 /** 720 * @name isValid 721 * 722 * @description a Playable isValid if it has an identifier and its type matches one of the valid MediaTypes (either 'audio' or 'live_audio') 723 * @example playable.isValid() 724 * @returns {boolean} true or false 725 * @methodOf Playable 726 */ 727 isValid : function () { 728 if (this.identifier !== null 729 && this.type !== null 730 && MediaTypes.isValid(this.type)) { 731 return true; 732 } 733 return false; 734 }, 735 /** 736 * @name isCustomScheme 737 * 738 * @description a Playable isCustomScheme if it has an identifier that matches one of the custom schemes defined 739 * @example playable.isCustomScheme('scheme_name') 740 * @returns {boolean} true or false 741 * @methodOf Playable 742 */ 743 isCustomScheme : function (scheme) { 744 if (custom_schemes.isScheme(this.identifier, scheme) === true) { 745 return true; 746 } 747 return false; 748 }, 749 /** 750 * @name isEOF 751 * 752 * @description a Playable is at the end of the file, return true. 753 * @example playable.isEOF() 754 * @returns {boolean} true or false 755 * @methodOf Playable 756 */ 757 isEOF : function () { 758 if (this.percent_played > 0.99995) { 759 return true; 760 } 761 return false; 762 }, 763 /** 764 * @name reset 765 * 766 * @description resets playable's position, percent_played, and percent_loaded back to 0 767 * @example playable.reset() 768 * @methodOf Playable 769 */ 770 reset : function () { 771 this.position = 0; 772 this.percent_played = 0; 773 this.percent_loaded = 0; 774 }, 775 /** 776 * @name setEmptyMembers 777 * 778 * @description finds and sets any provided members that are either '', 0, or null -- nothing will be set if passed params are empty or null 779 * @example playable.setEmptyMembers({object}) 780 * @methodOf Playable 781 */ 782 setEmptyMembers : function (params) { 783 var prop; 784 for (prop in params) { 785 if(params.hasOwnProperty(prop) 786 && this.hasOwnProperty(prop) 787 && params[prop] !== null 788 && params[prop] !== '' 789 && (this[prop] === null 790 || this[prop] === '' 791 || this[prop] === 0)) { 792 793 this[prop] = params[prop]; 794 } 795 } 796 }, 797 /** 798 * @name setMembers 799 * 800 * @description finds and sets any valid key, value pair-- will override anything that currently exists in that field. 801 * @example playable.setMembers({object}) 802 * @methodOf Playable 803 */ 804 setMembers : function (params) { 805 for (var prop in params) { 806 if (params.hasOwnProperty(prop) 807 && this.hasOwnProperty(prop)) { 808 this[prop] = params[prop]; 809 } 810 } 811 }, 812 /** 813 * @name clearFlashProperties 814 * 815 * @description removes the flash_server_url + flash_file_path 816 * so that only the progressive http mechanism is possible. 817 * @example playable.clearFlashProperties() 818 * @methodOf Playable 819 */ 820 clearFlashProperties : function () { 821 this.flash_server_url = ''; 822 this.flash_file_path = ''; 823 }, 824 /** 825 * @name isFlashStreamable 826 * 827 * @description returns whether or not flash_server_url + flash_file_path is avail. used to allow/disallow seeking before item loads. 828 * @returns {boolean} true or false 829 * @example playable.isFlashStreamable() 830 * @methodOf Playable 831 */ 832 isFlashStreamable : function () { 833 if (this.flash_server_url !== '' 834 && this.flash_file_path !== '') { 835 return true; 836 } 837 return false; 838 } 839 }; 840 841 842 /** 843 * @name APMPlayer 844 * @description main container for Audio/Video playback. 845 * @class 846 */ 847 var APMPlayer = function () { 848 var player = this; 849 850 var Audio = function () { 851 this.lib = soundManager; 852 this.init_status = false; 853 }; 854 Audio.prototype = { 855 init : function () { 856 if (player.audio.init_status === false) { 857 858 //soundManager2 settings 859 this.lib.flashVersion = 9; 860 this.lib.preferFlash = true; 861 this.lib.useHTML5Audio = true; 862 this.lib.consoleOnly = Debug.consoleOnly; 863 this.lib.debugMode = Debug.enabled; 864 this.lib.flashPollingInterval = 150; //helps improve seeker ui experience 865 this.lib.url = player.util.getLoadedScriptPathByFileName('soundmanager2') + 'swf/'; // used to dynamically determine lib location for flash lib dependency 866 867 this.lib.onready(function () { 868 if (player.audio.lib.html5Only === true) { //SM2 will know onready() if HTML5 is the only option avail. 869 player.mechanism.setSolutions([ player.mechanism.type.HTML5 ]); 870 Debug.log('Audio.init() -- setting to HTML5-only', Debug.type.info); 871 } 872 Debug.log('Audio.init() success', Debug.type.info); 873 player.audio.init_status = true; 874 player.internal_events.trigger(player.events.type.AUDIO_LIB_READY, {}); 875 }); 876 this.lib.ontimeout(function (status) { //(note: ontimeout typically only happens w/ flash-block, or flash not installed) 877 if(!player.audio.lib.canPlayMIME('audio/mp3') 878 && !player.audio.lib.canPlayMIME('audio/mpeg')) { 879 player.mechanism.setSolutions([]); //no possible playback solutions if no support for mp3 in HTML5 (eg Firefox). 880 } else { 881 player.mechanism.setSolutions([ player.mechanism.type.HTML5 ]); 882 } 883 player.audio.reset(); 884 }); 885 } else { 886 player.audio.reset(); 887 Debug.log('Audio.init() -- audio lib has already been initialized once, attempting reset', Debug.type.info); 888 } 889 }, 890 reset : function () { 891 player.audio.init_status = false; 892 893 var current_solution = player.mechanism.getCurrentSolution(); 894 switch (current_solution) { 895 case player.mechanism.type.FLASH: 896 player.audio.lib.preferFlash = true; 897 player.audio.lib.html5Only = false; 898 break; 899 case player.mechanism.type.HTML5: 900 player.audio.lib.preferFlash = false; 901 player.audio.lib.html5Only = true; 902 break; 903 default: 904 Debug.log('Audio.reset() no playback solution exists.', Debug.type.error); 905 player.events.trigger(player.events.type.PLAYER_FAILURE, null); 906 return false; 907 } 908 909 player.audio.lib.reboot(); 910 }, 911 load : function (playable) { 912 if (this.init_status === true) { 913 try { 914 var sound; 915 switch (player.mechanism.getCurrentSolution()) { 916 case player.mechanism.type.FLASH: 917 918 //1. create basic http-based sound first (http path, no serverURL) 919 sound = { 920 id: playable.identifier, 921 url: playable.http_file_path, 922 bufferTime: playable.buffer_time, 923 onconnect: function () { 924 Debug.log('Audio.load.lib.createSound.onConnect() - successfully connected over RTMP (' + playable.flash_server_url + ')', Debug.type.info); 925 player.internal_events.trigger(player.events.type.MEDIA_READY, playable); 926 }, 927 onfailure : function (sound) { 928 var playable = player.current_playable; 929 if (playable.position > 0) { 930 Debug.log('Audio.load.createSound.onfailure() -- network connection has been lost', Debug.type.info); 931 player.state.set(player.state.type.STOPPED, playable); 932 player.events.trigger(player.events.type.CONNECTION_LOST, playable); 933 } else if (sound.connected === true) { 934 Debug.log('Audio.load.createSound.onfailure() - requested file \'' + playable.flash_file_path + '\' w/ identifier \'' + playable.identifier + '\' could not be found in flash/RTMP mode.', Debug.type.error); 935 player.state.set(player.state.type.STOPPED, playable); 936 player.events.trigger(player.events.type.MISSING_FILE, playable); 937 player.events.trigger(player.events.type.FINISHED, playable); 938 } else { 939 //in this case, there was an issue connecting to our flash wowza server. as a back-up, falls-back to using HTML5-mode 940 Debug.log('Audio.load.createSound.onfailure() - could not connect to \'' + playable.flash_server_url + '\' ... falling back to HTML5-mode.', Debug.type.error); 941 player.state.set(player.state.type.STOPPED, playable); 942 player.mechanism.removeCurrentSolution(); 943 player.audio.reset(); 944 } 945 }, 946 onload : function(success) { 947 if(success === false) { //progressive download file failed to be loaded.. 948 var playable = player.current_playable; 949 player.events.trigger(player.events.type.MISSING_FILE, playable); 950 Debug.log('Audio.load.createSound.onload() - could not load \'' + playable.http_file_path + '\' over progressive download', Debug.type.error); 951 } 952 } 953 }; 954 955 //2. if flash server + file is present, stream it from flash server by 956 //overriding these settings before sound invocation 957 if (playable.flash_file_path !== null && playable.flash_file_path !== '' 958 && playable.flash_server_url !== null && playable.flash_server_url !== '') { 959 sound.serverURL = playable.flash_server_url; 960 sound.url = playable.flash_file_path; 961 } 962 963 this.lib.createSound(sound); 964 break; 965 966 case player.mechanism.type.HTML5: 967 968 //1. html5: much simpler 969 sound = this.lib.createSound({ 970 id: playable.identifier, 971 url: playable.http_file_path, 972 onload : function (success) { 973 var playable = player.current_playable; 974 975 if (!success) { 976 player.state.set(player.state.type.STOPPED, playable); 977 player.events.trigger(player.events.type.MISSING_FILE, playable); 978 player.events.trigger(player.events.type.FINISHED, playable); 979 Debug.log('Audio.load.createSound.onload(): requested file \'' + playable.http_file_path + '\' w/ identifier \'' + playable.identifier + '\' could not be found in HTML5 mode.', Debug.type.error); 980 } else { 981 if (this.duration) { 982 playable.duration = this.duration; 983 Debug.log('Audio.load.createSound.onload(): duration found after start', Debug.type.info); 984 } else { 985 Debug.log('Audio.load.createSound.onload(): duration unknown', Debug.type.info); 986 } 987 } 988 } 989 }); 990 991 player.internal_events.trigger(player.events.type.MEDIA_READY, playable); 992 break; 993 994 default: 995 Debug.log('Audio.load() no playback solution exists.', Debug.type.error); 996 player.events.trigger(player.events.type.PLAYER_FAILURE, playable); 997 break; 998 } 999 1000 } catch (e) { 1001 Debug.log('Exception thrown in APMPlayer.Audio.load : ' + e.toString(), Debug.type.error); 1002 } 1003 } else { 1004 if(player.mechanism.getCurrentSolution() === null) { 1005 player.events.trigger(player.events.type.PLAYER_FAILURE, playable); 1006 } else { 1007 Debug.log('Audio.lib.load - audio lib not initialized. load() will be called again when player is finally initialized.', Debug.type.info); 1008 } 1009 } 1010 }, 1011 unload : function (playable) { 1012 if (player.current_playable !== null) { 1013 Debug.log('Audio.unload() about to stop, drop and roll current sound.', Debug.type.info); 1014 this.lib.destroySound(playable.identifier); 1015 playable.reset(); 1016 player.state.set(player.state.type.STOPPED, playable); 1017 player.events.trigger(player.events.type.UNLOADED, playable); 1018 } 1019 }, 1020 play : function (playable) { 1021 if (!this.lib.getSoundById(playable.identifier)) { 1022 this.load(playable); 1023 } else { 1024 Debug.log('Audio.play() attempting to play from lib.', Debug.type.info); 1025 1026 this.lib.play(playable.identifier, { 1027 volume : (player.settings.volume * 100), 1028 position : playable.position, 1029 onplay : function () { 1030 player.state.set(player.state.type.PLAYING, playable); 1031 player.events.trigger(player.events.type.PLAYING, playable); 1032 player.events.trigger(player.events.type.METADATA, playable); 1033 if (player.settings.muted) { 1034 this.mute(); 1035 } 1036 Debug.log('Audio.play.onplay() PLAYING fired', Debug.type.info); 1037 }, 1038 onpause: function () { 1039 player.state.set(player.state.type.PAUSED, playable); 1040 player.events.trigger(player.events.type.PAUSED, playable); 1041 Debug.log('Audio.play.onpause() PAUSED fired', Debug.type.info); 1042 }, 1043 onresume : function () { 1044 player.state.set(player.state.type.PLAYING, playable); 1045 player.events.trigger(player.events.type.PLAYING, playable); 1046 Debug.log('Audio.play.onresume() PLAYING fired', Debug.type.info); 1047 }, 1048 onfinish : function () { 1049 player.current_playable.reset(); 1050 player.state.set(player.state.type.STOPPED, playable); 1051 player.events.trigger(player.events.type.FINISHED, playable); 1052 Debug.log('Audio.play.onfinish() FINISHED fired; playable reset.', Debug.type.info); 1053 }, 1054 onbufferchange : function () { 1055 if (this.isBuffering === true) { 1056 player.events.trigger(player.events.type.BUFFER_START, playable); 1057 Debug.log('Audio.play.onbufferchange() BUFFER_START fired ', Debug.type.info); 1058 } else { 1059 player.events.trigger(player.events.type.BUFFER_END, playable); 1060 Debug.log('Audio.play.onbufferchange() BUFFER_END fired ', Debug.type.info); 1061 } 1062 }, 1063 whileplaying : function () { 1064 1065 var playable = player.current_playable; 1066 if (playable.type === MediaTypes.type.LIVE_AUDIO) { 1067 playable.percent_played = 1; 1068 playable.duration = 0; 1069 playable.position = this.position; 1070 1071 player.events.trigger(player.events.type.POSITION_UPDATE, playable); 1072 1073 } else { //STATIC AUDIO 1074 if (this.position !== 0) { //SM2 returns position 0 once after restarting a piece in middle, skip this if/when it happens. 1075 playable.position = this.position; 1076 } 1077 1078 if (this.duration !== 0 1079 && this.duration > playable.duration) { //helps w/ HTTP progressive download and duration display. 1080 playable.duration = this.duration; 1081 } 1082 1083 if (this.durationEstimate > this.duration) { 1084 //loading over HTTP progressive download, add percent loaded. 1085 playable.percent_loaded = this.duration / this.durationEstimate; 1086 } else if (playable.percent_loaded > 0 && playable.percent_loaded < 1) { 1087 playable.percent_loaded = 1; 1088 } 1089 1090 if (playable.duration > 0) { 1091 playable.percent_played = playable.position / playable.duration; 1092 } 1093 1094 //SM2 'work-around'-- the onfinish() event does not always fire if scrubbing to end. 1095 //this catches that scenario 1096 if (playable.isEOF()) { 1097 playable.percent_played = 1; 1098 playable.position = playable.duration; 1099 //player.events.trigger(player.events.type.FINISHED, playable); 5/21/12 -- aml removed this due to double-advances in iOS. 1100 //onfinish() now appears to fire normally in all cases. 1101 //Debug.log('Audio.play.whileplaying() FINISHED fired. ' + playable.title, Debug.type.info); 1102 } else { 1103 //Debug.log('Audio.play.whileplaying() POSITION_UPDATE fired. ' + playable.percent_loaded + '% ' + playable.percent_played, Debug.type.info); 1104 player.events.trigger(player.events.type.POSITION_UPDATE, playable); 1105 } 1106 } 1107 }, 1108 onmetadata : function () { 1109 if (this.hasOwnProperty('metadata')) { 1110 if (this.metadata.hasOwnProperty('adw_ad') //ads wizz add 1111 && this.metadata.adw_ad === 'true' 1112 && this.metadata.hasOwnProperty('metadata') 1113 && this.metadata.metadata.indexOf("adswizzContext") !== -1) { 1114 1115 Debug.log('onmetadata() received adw_ad of insertionType: \'' + this.metadata.insertionType + '\'', Debug.type.info); 1116 1117 playable.title = 'adw_ad_' + this.metadata.insertionType; 1118 playable.adw_context = this.metadata.metadata.substr(15); //hack via AdsWizz. need to request a better solution 1119 player.events.trigger(player.events.type.METADATA, playable); 1120 } 1121 else if (this.metadata.hasOwnProperty('StreamTitle') //APM-specific 1122 && typeof this.metadata.StreamTitle !== 'undefined') { 1123 Debug.log('onmetadata() received metadata w/ title: \'' + this.metadata.StreamTitle + '\'', Debug.type.info); 1124 playable.title = this.metadata.StreamTitle; 1125 player.events.trigger(player.events.type.METADATA, playable); 1126 } 1127 } 1128 } 1129 }); 1130 } 1131 }, 1132 1133 pause : function (playable) { 1134 var sound = this.lib.getSoundById(playable.identifier); 1135 if (sound) { 1136 sound.pause(); 1137 return true; 1138 } 1139 Debug.log('Audio.pause() Error. Could not pause. \'' + playable.identifier + '\' is unknown.', Debug.type.warn); 1140 return false; 1141 }, 1142 1143 unpause : function (playable) { 1144 var sound = this.lib.getSoundById(playable.identifier); 1145 if (sound && sound.paused === true) { 1146 sound.resume(); 1147 return true; 1148 } 1149 Debug.log('Audio.unpause() Error. Could not unpause. \'' + playable.identifier + '\' is unknown.', Debug.type.warn); 1150 return false; 1151 }, 1152 1153 seek : function (playable, percent_decimal) { 1154 var sound = this.lib.getSoundById(playable.identifier); 1155 if (sound) { 1156 if (playable.duration) { 1157 Debug.log('Audio.seek() seeking to \'' + percent_decimal + '\' of sound \'' + playable.identifier + '\'', Debug.type.info); 1158 var msec = percent_decimal * playable.duration; 1159 sound.setPosition(msec); 1160 return true; 1161 } 1162 Debug.log('Audio.seek() Error. Could not seek. duration of \'' + playable.identifier + '\' is unknown.', Debug.type.warn); 1163 return false; 1164 } 1165 1166 Debug.log('Audio.seek() sound \'' + playable.identifier + '\' is unknown.', Debug.type.warn); 1167 return false; 1168 }, 1169 1170 mute : function (playable) { 1171 var sound = this.lib.getSoundById(playable.identifier); 1172 if (sound) { 1173 sound.mute(); 1174 } 1175 }, 1176 1177 unmute : function (playable) { 1178 var sound = this.lib.getSoundById(playable.identifier); 1179 if (sound) { 1180 sound.unmute(); 1181 } 1182 }, 1183 1184 setVolume : function (playable, percent_decimal) { 1185 var percent = percent_decimal * 100; 1186 var sound = this.lib.getSoundById(playable.identifier); 1187 if (sound) { 1188 Debug.log('Audio.setVolume() setting volume to ' + percent + '% (out of 100)', Debug.type.info); 1189 sound.setVolume(percent); 1190 } else { 1191 Debug.log('Audio.setVolume() sound is not loaded. volume will be set to ' + percent + '% once audio begins playing', Debug.type.info); 1192 } 1193 } 1194 1195 }; //end Audio() 1196 1197 //var Video = function () {}; 1198 1199 //default settings 1200 this.settings = { 1201 volume : 0.9, 1202 muted : false, 1203 debug : false 1204 }; 1205 1206 this.current_playable = null; //only one single playable can be in use at a given time 1207 1208 this.events = new Events(); 1209 this.internal_events = new Events(); 1210 this.mechanism = new PlaybackMechanism(); 1211 this.state = new PlaybackState(); 1212 this.audio = new Audio(); 1213 this.internal_event_handlers = { 1214 checkReady : function () { 1215 if (player.audio.init_status === true) { 1216 Debug.log('checkReady() player ready. all dependencies loaded.', Debug.type.info); 1217 player.events.trigger(player.events.type.PLAYER_READY, {}); 1218 } else { 1219 //will be useful when multiple dependencies are required on init 1220 Debug.log('checkReady() not quite ready -- waiting for other dependencies to load...', Debug.type.info); 1221 } 1222 }, 1223 onMediaReady : function (playable) { 1224 player.audio.play(playable); 1225 1226 } 1227 }; 1228 this.internal_events.addListener(player.events.type.AUDIO_LIB_READY, player.internal_event_handlers.checkReady); 1229 this.internal_events.addListener(player.events.type.MEDIA_READY, player.internal_event_handlers.onMediaReady); 1230 1231 this.util = { 1232 getParameterByName : function (name) { 1233 name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); 1234 var regexS = "[\\?&]" + name + "=([^]*)"; 1235 var regex = new RegExp(regexS); 1236 var results = regex.exec(window.location.href); 1237 1238 if (results === null) { 1239 return ""; 1240 } 1241 1242 return decodeURIComponent(results[1].replace(/\+/g, " ")); 1243 }, 1244 getLoadedScriptPathByFileName : function (name) { 1245 var scripts = document.getElementsByTagName("script"); 1246 var numScripts = scripts.length; 1247 var index; 1248 for (index = 0; index < numScripts; index += 1) { 1249 var match = scripts[index].src.indexOf(name, 0); 1250 if (match !== -1) { 1251 return scripts[index].src.slice(0, match); 1252 } 1253 } 1254 }, 1255 getProjectBasePath : function () { 1256 var path = player.util.getLoadedScriptPathByFileName('script/apmplayer-all.min.js'); 1257 if (typeof path === 'undefined') { 1258 path = player.util.getLoadedScriptPathByFileName('script/apmplayer.js'); 1259 } 1260 return path; 1261 }, 1262 mergeSettings : function (settings) { 1263 var prop; 1264 for (prop in settings) { 1265 if (settings.hasOwnProperty(prop) 1266 && player.settings.hasOwnProperty(prop)) { 1267 player.settings[prop] = settings[prop]; 1268 } 1269 } 1270 }, 1271 checkDebug : function () { 1272 if (player.util.getParameterByName('debug')) { 1273 Debug.enabled = true; 1274 } 1275 1276 if (player.util.getParameterByName('debug') === 'all') { 1277 Debug.consoleOnly = false; 1278 } 1279 }, 1280 addIEFixes : function () { 1281 if (!Array.prototype.indexOf) { 1282 Array.prototype.indexOf = function(elt /*, from*/) { 1283 var len = this.length >>> 0; 1284 1285 var from = Number(arguments[1]) || 0; 1286 from = (from < 0) 1287 ? Math.ceil(from) 1288 : Math.floor(from); 1289 if (from < 0) 1290 from += len; 1291 1292 for (; from < len; from++) { 1293 if (from in this && 1294 this[from] === elt) 1295 return from; 1296 } 1297 return -1; 1298 }; 1299 } 1300 } 1301 }; 1302 1303 return { //APMPlayer public methods 1304 /** 1305 * @name init 1306 * @description initializes APMPlayer library; NOTE: init() must be called immediately after instantiation to ensure SoundManager2 init doesn't timeout 1307 * @methodOf APMPlayer 1308 */ 1309 init : function () { 1310 player.util.addIEFixes(); 1311 player.util.checkDebug(); 1312 player.audio.init(); 1313 }, 1314 /** 1315 * @name reset 1316 * @description resets APMPlayer -- sets {@link PlaybackMechanism} solutions + resets Audio playback library 1317 * 1318 * @param {string[]} playback_solutions array of accepted playback mechanisms to use (ie FLASH, HTML5) 1319 * 1320 * @methodOf APMPlayer 1321 */ 1322 reset : function (playback_solutions) { 1323 if (playback_solutions instanceof Array) { 1324 player.mechanism.setSolutions(playback_solutions); 1325 } 1326 player.audio.reset(); 1327 }, 1328 /** 1329 * @name play 1330 * @description loads item (if not loaded), then plays it-- automatically unloads any previous {@link Playable} that was loaded/playing 1331 * @fires Events.type.PLAYING will fire when audio successfully starts playing. (see {@link Events}). 1332 * @methodOf APMPlayer 1333 */ 1334 play : function (playable, settings) { 1335 if (playable instanceof Playable) { 1336 if (playable === player.current_playable 1337 && player.state.current() !== player.state.type.PLAYING) { 1338 switch (playable.type) { 1339 case MediaTypes.type.AUDIO: 1340 case MediaTypes.type.LIVE_AUDIO: 1341 player.audio.play(playable); 1342 break; 1343 case MediaTypes.type.VIDEO: 1344 break; 1345 default: 1346 Debug.log('play() unsupported type: \'' + playable.type + '\'', Debug.type.error); 1347 } 1348 } else if (playable !== player.current_playable) { 1349 1350 //1. check if playback solutions exist. 1351 if(player.mechanism.getCurrentSolution() === null) { 1352 Debug.log('play() insufficient playback mechanism for platform. Triggered PLAYER_FAILURE', Debug.type.error); 1353 player.events.trigger(player.events.type.PLAYER_FAILURE, playable); 1354 return false; 1355 } 1356 1357 //2. unload item if a current one exists. 1358 if (player.current_playable !== null) { 1359 player.audio.unload(player.current_playable); 1360 } 1361 1362 player.util.mergeSettings(settings); 1363 player.current_playable = playable; 1364 1365 switch (playable.type) { 1366 case MediaTypes.type.AUDIO: 1367 case MediaTypes.type.LIVE_AUDIO: 1368 player.audio.load(playable); 1369 break; 1370 case MediaTypes.type.VIDEO: 1371 break; 1372 default: 1373 Debug.log('load() unsupported type: \'' + playable.type + '\'', Debug.type.error); 1374 break; 1375 } 1376 } 1377 } else { 1378 Debug.log('play() invalid playable passed. must of of type Playable. did nothing.', Debug.type.error); 1379 return false; 1380 } 1381 }, 1382 /** 1383 * @name pause 1384 * @description if static audio, pauses currently loaded {@link Playable}; However, if type LIVE_AUDIO, the playable is automatically unloaded. 1385 * @fires Events.type.PAUSED, or Events.type.UNLOADED will fire depending on type, on success. (see {@link Events}). 1386 * @methodOf APMPlayer 1387 */ 1388 pause : function () { 1389 var playable = player.current_playable; 1390 if (playable !== null) { 1391 switch (playable.type) { 1392 case MediaTypes.type.AUDIO: 1393 player.audio.pause(playable); 1394 break; 1395 case MediaTypes.type.LIVE_AUDIO: 1396 player.audio.unload(playable); 1397 break; 1398 case MediaTypes.type.VIDEO: 1399 break; 1400 default: 1401 Debug.log('pause() unsupported type: \'' + playable.type + '\'', Debug.type.error); 1402 } 1403 } else { 1404 Debug.log('pause() no current playable loaded. nothing to pause.', Debug.type.warn); 1405 } 1406 }, 1407 /** 1408 * @name unload 1409 * @description stops, unloads, and destroys the current {@link Playable} 1410 * @fires Events.type.UNLOADED will be fired if audio successfully unloads (see {@link Events}). 1411 * @methodOf APMPlayer 1412 */ 1413 unload : function () { 1414 var playable = player.current_playable; 1415 if (playable !== null) { 1416 switch (playable.type) { 1417 case MediaTypes.type.AUDIO: 1418 case MediaTypes.type.LIVE_AUDIO: 1419 player.audio.unload(playable); 1420 break; 1421 case MediaTypes.type.VIDEO: 1422 break; 1423 default: 1424 Debug.log('unload() unsupported type: \'' + playable.type + '\'', Debug.type.error); 1425 } 1426 } else { 1427 Debug.log('unload() no current playable loaded. nothing to stop/unload.', Debug.type.info); 1428 } 1429 }, 1430 /** 1431 * @name seek 1432 * @description moves play head to current percentage of media {@link Playable}. 1433 * @fires Events.type.POSITION_UPDATE will continue to fire after position is updated by seek() (see {@link Events}). 1434 * @param {number} percent_decimal the point in the media file to seek to. (eg, 0 is beginning / .50 is half-way / 1 is end) 1435 * @methodOf APMPlayer 1436 */ 1437 seek : function (percent_decimal) { 1438 var playable = player.current_playable; 1439 if (playable !== null) { 1440 switch (playable.type) { 1441 case MediaTypes.type.AUDIO: 1442 player.audio.seek(playable, percent_decimal); 1443 break; 1444 case MediaTypes.type.LIVE_AUDIO: 1445 Debug.log('seek() sorry, this item is not seekable \'' + playable.identifier + '\', type: \'' + playable.type + '\'', Debug.type.info); 1446 break; 1447 case MediaTypes.type.VIDEO: 1448 break; 1449 default: 1450 Debug.log('seek() unsupported type: \'' + playable.type + '\'', Debug.type.error); 1451 } 1452 } else { 1453 Debug.log('seek() no current playable loaded. nothing to seek.', Debug.type.info); 1454 } 1455 }, 1456 /** 1457 * @name mute 1458 * @description mutes sound; returns nothing, triggers nothing. 1459 * @methodOf APMPlayer 1460 */ 1461 mute : function () { 1462 player.settings.muted = true; 1463 1464 var playable = player.current_playable; 1465 if (playable !== null) { 1466 switch (playable.type) { 1467 case MediaTypes.type.AUDIO: 1468 case MediaTypes.type.LIVE_AUDIO: 1469 player.audio.mute(playable); 1470 break; 1471 case MediaTypes.type.VIDEO: 1472 break; 1473 default: 1474 Debug.log('mute() unsupported type: \'' + playable.type + '\'', Debug.type.error); 1475 break; 1476 } 1477 } 1478 Debug.log('mute() -- player is now muted.', Debug.type.info); 1479 }, 1480 /** 1481 * @name unmute 1482 * @description unmutes sound; returns nothing, triggers nothing. 1483 * @methodOf APMPlayer 1484 */ 1485 unmute : function () { 1486 player.settings.muted = false; 1487 1488 var playable = player.current_playable; 1489 if (playable !== null) { 1490 switch (playable.type) { 1491 case MediaTypes.type.AUDIO: 1492 case MediaTypes.type.LIVE_AUDIO: 1493 player.audio.unmute(playable); 1494 break; 1495 case MediaTypes.type.VIDEO: 1496 break; 1497 default: 1498 Debug.log('unmute() unsupported type: \'' + playable.type + '\'', Debug.type.error); 1499 break; 1500 } 1501 } 1502 Debug.log('unmute() -- player is now unmuted.', Debug.type.info); 1503 }, 1504 /** 1505 * @name setVolume 1506 * @description sets volume. 1507 * @fires Events.type.VOLUME_UPDATED fires event after sucessfully setting volume (see {@link Events}). 1508 * @param {number} percent_decimal the percentage to set the volume to 0 to 1 1509 * @methodOf APMPlayer 1510 */ 1511 setVolume : function (percent_decimal) { 1512 if (percent_decimal < 0) { 1513 percent_decimal = 0; 1514 Debug.log('setVolume() invalid percent_decimal passed: \'' + percent_decimal + '\' is less than 0. percent_decimal set to 0. percentages must be represented as a decimal from 0 to 1 (eg .45)', Debug.type.warn); 1515 } else if (percent_decimal > 1) { 1516 percent_decimal = 1; 1517 Debug.log('setVolume() invalid percent_decimal passed: \'' + percent_decimal + '\' is greater than 1. percent_decimal set to 1.00 by default. percentages must be represented as a decimal from 0.00 to 1.00 (eg .45)', Debug.type.warn); 1518 } 1519 1520 var playable = player.current_playable; 1521 if (playable !== null) { 1522 switch (playable.type) { 1523 case MediaTypes.type.AUDIO: 1524 case MediaTypes.type.LIVE_AUDIO: 1525 player.audio.setVolume(playable, percent_decimal); 1526 break; 1527 case MediaTypes.type.VIDEO: 1528 break; 1529 default: 1530 Debug.log('setVolume() unsupported type: \'' + playable.type + '\'', Debug.type.error); 1531 } 1532 } else { 1533 Debug.log('setVolume() no playable loaded. VOLUME_UPDATED event still fired. new vox : \'' + percent_decimal + '\'', Debug.type.info); 1534 } 1535 player.settings.volume = percent_decimal; 1536 player.events.trigger(player.events.type.VOLUME_UPDATED, { 'percent_decimal' : percent_decimal }); 1537 }, 1538 /** 1539 * @name debug 1540 * @description reference to APMPlayer {@link Debug} logger 1541 * 1542 * @example APMPlayer.debug.log('debug message', APMPlayer.debug.type.info, 'ObjectName'); 1543 * 1544 * Debug types: 1545 * APMPlayer.debug.type.info 1546 * APMPlayer.debug.type.warn 1547 * APMPlayer.debug.type.error 1548 * 1549 * @static 1550 * @fieldOf APMPlayer 1551 */ 1552 debug : Debug, 1553 /** 1554 * @name events 1555 * @description reference to the main {@link Events} object for APMPlayer 1556 * @example APMPlayer.events.addListener(APMPlayer.events.type.PLAYER_READY, function() {} ); 1557 * @fieldOf APMPlayer 1558 */ 1559 events : player.events, 1560 /** 1561 * @name mechanism 1562 * @description reference to internal {@link PlaybackMechanism} object. 1563 * @example APMPlayer.mechanism.getCurrentSolution(); 1564 * @fieldOf APMPlayer 1565 */ 1566 mechanism : player.mechanism, 1567 /** 1568 * @name mediaTypes 1569 * @description reference to supported MediaTypes (currently AUDIO, LIVE_AUDIO) 1570 * @example player_ui.playlist.current().type == APMPlayer.mediaTypes.LIVE_AUDIO 1571 * @fieldOf APMPlayer 1572 */ 1573 mediaTypes : MediaTypes.type, 1574 /** 1575 * @name state 1576 * @description reference to current {@link PlaybackState} object 1577 * @example APMPlayer.state.getCurrent() === APMPlayer.state.type.PLAYING; 1578 * @fieldOf APMPlayer 1579 */ 1580 state : player.state, 1581 /** 1582 * @name base_path 1583 * @description returns the base_path of the project, relative to this file. 1584 * checks for both apmplayer-all.min.js and apmplayer.js 1585 * @example http://localhost/apmplayer/1.2/ 1586 * @fieldOf APMPlayer 1587 */ 1588 base_path : player.util.getProjectBasePath() 1589 }; 1590 }; //end APMPlayer() 1591 1592 /** 1593 * @name Playlist 1594 * @description structured, organized playlist made-up of {@link Playable} objects. 1595 * @constructor 1596 * */ 1597 var Playlist = function () { 1598 /** 1599 * @name events 1600 * @description object reference to specific {@link Events} object for Playlist 1601 * @example Playlist.events.addListener(APMPlayer.events.type.PLAYLIST_CURRENT_CHANGE, function() {} ); 1602 * @fieldOf Playlist 1603 */ 1604 this.events = new Events(); 1605 this._items = []; 1606 this._current_index = null; 1607 }; 1608 Playlist.prototype = { 1609 /** 1610 * @name add 1611 * @description adds a valid {@link Playable} to the playlist. 1612 * @param {Playable} playable the playable to add. 1613 * @example Playlist.add(playable); 1614 * @returns {boolean} success or failure. 1615 * @methodOf Playlist 1616 */ 1617 add : function (playable) { 1618 if (playable instanceof Playable 1619 && playable.isValid()) { 1620 1621 if (this.item(playable.identifier) !== null) { 1622 Debug.log('add() could not add \'' + playable.identifier + '\' to playlist because it already exists!', Debug.type.warn, 'Playlist'); 1623 return false; 1624 } 1625 1626 this._items.push(playable); 1627 if (this._current_index === null) { 1628 this._current_index = 0; 1629 this.events.trigger(this.events.type.PLAYLIST_CURRENT_CHANGE, null); 1630 } 1631 Debug.log('add() new playable successfully added to playlist: \'' + playable.identifier + '\'', Debug.type.info, 'Playlist'); 1632 return true; 1633 } 1634 1635 Debug.log('add() -- error: nothing added to playlist. either object passed was not of type Playable or identifier \'' + playable.identifier + '\' is invalid.', Debug.type.warn, 'Playlist'); 1636 return false; 1637 }, 1638 /** 1639 * @name _count 1640 * @description returns number of items in playlist 1641 * @private 1642 * @ignore 1643 */ 1644 _count : function () { 1645 return this._items.length; 1646 }, 1647 /** 1648 * @name current 1649 * @description returns the current {@link Playable} in the playlist. 1650 * @returns {Playable} returns a valid playable, or null if nothing in playlist. 1651 * @example Playlist.current(); 1652 * @methodOf Playlist 1653 */ 1654 current : function () { 1655 if (this._current_index !== null) { 1656 return this._items[this._current_index]; 1657 } 1658 return null; 1659 }, 1660 /** 1661 * @name item 1662 * @description returns specific {@link Playable} by identifier, if it exists in playlist 1663 * @param {string} identifier of a specific {@link Playable}. 1664 * @returns {Playable} if found, returns a valid playable, or null if non-existant. 1665 * @example Playlist.item('kick_ass_rock_song'); 1666 * @methodOf Playlist 1667 */ 1668 item : function (identifier) { 1669 var j, total_items = this._count(); 1670 for (j = 0; j < total_items; j += 1) { 1671 if (this._items[j].identifier === identifier) { 1672 return this._items[j]; 1673 } 1674 } 1675 return null; 1676 }, 1677 /** 1678 * @name goto 1679 * @description finds appropriate {@link Playable} in playlist and switches current pointer to that playable, if it exists in the playlist. 1680 * @param {string} identifier of a specific {@link Playable}. 1681 * @returns {boolean} success or failure. 1682 * @example Playlist.goto('boring_bluegrass_song'); 1683 * @fires Events.type.PLAYLIST_CURRENT_CHANGE fires upon successful update of current playlist item (see {@link Events}). 1684 * @methodOf Playlist 1685 */ 1686 goto : function (identifier) { 1687 var j, total_items = this._count(); 1688 for (j = 0; j < total_items; j += 1) { 1689 if (this._items[j].identifier === identifier) { 1690 var previous_playable = this.current(); 1691 this._current_index = j; 1692 this.events.trigger(this.events.type.PLAYLIST_CURRENT_CHANGE, previous_playable); 1693 return true; 1694 } 1695 } 1696 Debug.log('goto() - invalid identifier passed \'' + identifier + '\'. This was not found in the current playlist!', Debug.type.warn, 'Playlist'); 1697 return false; 1698 }, 1699 /** 1700 * @name hasNext 1701 * @description returns whether or not there is at least one more item before end of playlist 1702 * @returns {boolean} true if playlist has another item, false if at end of playlist 1703 * @example Playlist.hasNext(); 1704 * @methodOf Playlist 1705 */ 1706 hasNext : function () { 1707 if (this._current_index + 1 < this._count()) { 1708 return true; 1709 } 1710 return false; 1711 }, 1712 /** 1713 * @name remove 1714 * @description finds and removes a specifc {@link Playable} in playlist by identifier. The current playlist item may not be removed. 1715 * @param {string} identifier of a specific {@link Playable}. 1716 * @returns {boolean} success or failure. 1717 * @example Playlist.remove('brittany_spears_theft_of_the_dial'); 1718 * @methodOf Playlist 1719 */ 1720 remove : function (identifier) { 1721 var j, num_items_before_remove = this._count(); 1722 for (j = 0; j < num_items_before_remove; j += 1) { 1723 1724 if (this._items[j].identifier === identifier) { 1725 1726 if (this.current().identifier === identifier) { 1727 Debug.log('remove() -- sorry, you may not remove the current item in the playlist. returning false.)', Debug.type.warn, 'Playlist'); 1728 return false; 1729 } 1730 1731 this._items.splice(j, 1); //remove from array, while resetting array indexes 1732 1733 if (this._current_index > 0 1734 && j <= this._current_index) { 1735 this._current_index -= 1; //move current index back by 1, if we purged something before current item 1736 } 1737 1738 return true; 1739 } 1740 } 1741 return false; 1742 }, 1743 /** 1744 * @name next 1745 * @description advances current() point to next {@link Playable} in playlist. Also moves current to beginning if at end of playlist. 1746 * @returns {boolean} false if no items exist in playlist. 1747 * @example Playlist.next(); 1748 * @fires Events.type.PLAYLIST_CURRENT_CHANGE fires upon successful change of current playlist item (see {@link Events}). Also passes previous_playable back with the Event. 1749 * @methodOf Playlist 1750 */ 1751 next : function () { 1752 if (this._current_index !== null) { 1753 Debug.log('next() advancing to next position in playlist (or to beginning if at last)', Debug.type.info, 'Playlist'); 1754 var previous_playable = this.current(); 1755 this._current_index = (this._current_index + 1 < this._count()) ? this._current_index + 1 : 0; 1756 this.events.trigger(this.events.type.PLAYLIST_CURRENT_CHANGE, previous_playable); 1757 } else { 1758 return false; 1759 } 1760 }, 1761 /** 1762 * @name previous 1763 * @description sets current() point to back one {@link Playable} in playlist. Also moves current to end if at beginning of playlist. 1764 * @returns {boolean} false if no items exist in playlist. 1765 * @example Playlist.previous(); 1766 * @fires Events.type.PLAYLIST_CURRENT_CHANGE fires upon successful change of current playlist item (see {@link Events}). Also passes previous_playable back with the Event. 1767 * @methodOf Playlist 1768 */ 1769 previous : function () { 1770 if (this._current_index !== null) { 1771 Debug.log('previous() moving to previous position in playlist (or to last if at beginning)', Debug.type.info, 'Playlist'); 1772 var previous_playable = this.current(); 1773 this._current_index = (this._current_index - 1 >= 0) ? this._current_index - 1 : this._count() - 1; 1774 this.events.trigger(this.events.type.PLAYLIST_CURRENT_CHANGE, previous_playable); 1775 } else { 1776 return false; 1777 } 1778 } 1779 }; //end Playlist 1780 1781 1782 var apmplayer_instance; //singleton 1783 return { 1784 /** 1785 * @name getPlayer 1786 * @description returns singleton instance of {@link APMPlayer}. 1787 * @returns {APMPlayer} 1788 * @methodOf APMPlayerFactory 1789 */ 1790 getPlayer: function () { 1791 if (typeof apmplayer_instance === 'undefined') { 1792 apmplayer_instance = new APMPlayer(); 1793 apmplayer_instance.constructor = null; //prohibit new's 1794 } 1795 return apmplayer_instance; 1796 }, 1797 /** 1798 * @name getPlayable 1799 * @description validates params argument and constructs a new {@link Playable} object upon each call, if valid. Returns an empty {@link Playable} if passed arguments are invalid. see also {@link Playable.isValid} 1800 * @param {Object} params object literal used to build Playable. 1801 * @returns {Playable} 1802 * 1803 * @example regular example: 1804 * APMPlayerFactory.getPlayable( 1805 * { 1806 * type: 'audio', 1807 * identifier: 'my_audio', 1808 * flash_server_url: 'rtmp://server/', 1809 * flash_file_path: 'mp3:path/file.mp3', 1810 * http_file_path: 'http://server/file.mp3', 1811 * buffer_time: 5 1812 * }); 1813 * 1814 * @example CustomScheme example: 1815 * APMPlayerFactory.getPlayable( 1816 * { 1817 * identifier: 'apm_audio:/being/programs/2011/12/15/20111222_prophetic_imagination_128.mp3' 1818 * }); 1819 * 1820 * (note that in this example, the CustomScheme provides attributes that are pre-defined 1821 * (eg, flash_server_url, flash_file_path, http_file_path, type, buffer_time) 1822 * 1823 * 1824 * @methodOf APMPlayerFactory 1825 */ 1826 getPlayable : function (params) { 1827 return new Playable(params); 1828 }, 1829 /** 1830 * @name getPlaylist 1831 * @description constructs and returns a new {@link Playlist}. 1832 * @returns {Playlist} 1833 * @methodOf APMPlayerFactory 1834 */ 1835 getPlaylist : function () { 1836 return new Playlist(); 1837 }, 1838 /** 1839 * @name getCustomSchemes 1840 * @description returns a CustomSchemes object to initialize the map of {@link CustomSchemes}, used as a short-cut for constructing {@link Playable} objects -- see {@link CustomSchemes} for more detail. 1841 * @param {Object} params object literal used to initialize {@link CustomSchemes} -- the object can contain any number of attributes already defined for {@link Playable} objects. 1842 * @returns {CustomSchemes} 1843 * 1844 * @methodOf APMPlayerFactory 1845 */ 1846 getCustomSchemes : function () { 1847 return custom_schemes; 1848 } 1849 }; 1850 }()); //end APMPlayerFactory 1851 } 1852 1853 //initialization w/ custom configuration short-cuts (not required) 1854 var scheme_map = { 1855 apm_audio : { 1856 flash_server_url : 'rtmp://archivemedia.publicradio.org/music', 1857 flash_file_prefix : 'mp3:ondemand', 1858 http_file_prefix : 'http://ondemand.publicradio.org', 1859 buffer_time : 3, 1860 type : 'audio' 1861 }, 1862 apm_live_audio : { 1863 mpr_news : { 1864 flash_server_url : 'rtmp://archivemedia.publicradio.org/news', 1865 flash_file_path : 'news.stream', 1866 http_file_path : 'http://newsstream1.publicradio.org:80/', 1867 buffer_time : 6, 1868 type : 'live_audio' 1869 }, 1870 mpr_current : { 1871 flash_server_url : 'rtmp://archivemedia.publicradio.org/kcmp', 1872 flash_file_path : 'kcmp.stream', 1873 http_file_path : 'http://currentstream1.publicradio.org:80/', 1874 buffer_time : 6, 1875 type : 'live_audio' 1876 }, 1877 csf : { 1878 flash_server_url : 'rtmp://archivemedia.publicradio.org/csf', 1879 flash_file_path : 'csf.stream', 1880 http_file_path : 'http://204.93.222.85:80/csf-nopreroll', 1881 buffer_time : 6, 1882 type : 'live_audio' 1883 }, 1884 wpbi : { 1885 flash_server_url : 'rtmp://archivemedia.publicradio.org/wpbistream', 1886 flash_file_path : 'wpbi.stream', 1887 http_file_path : 'http://wpbistream1.lbdns-streamguys.com:80/wpbistream', 1888 buffer_time : 6, 1889 type : 'live_audio' 1890 } 1891 } 1892 }; 1893 var custom_schemes = APMPlayerFactory.getCustomSchemes(); 1894 custom_schemes.init(scheme_map); 1895 1896 //initialize player lib 1897 var APMPlayer = APMPlayerFactory.getPlayer(); 1898 APMPlayer.init(); 1899 1900