1/* 2 * jPlayer Player Plugin for Popcorn JavaScript Library 3 * http://www.jplayer.org 4 * 5 * Copyright (c) 2012 - 2014 Happyworm Ltd 6 * Licensed under the MIT license. 7 * http://opensource.org/licenses/MIT 8 * 9 * Author: Mark J Panaghiston 10 * Version: 1.1.6 11 * Date: 27th November 2014 12 * 13 * For Popcorn Version: 1.3 14 * For jPlayer Version: 2.9.0 15 * Requires: jQuery 1.7+ 16 * Note: jQuery dependancy cannot be removed since jPlayer 2 is a jQuery plugin. Use of jQuery will be kept to a minimum. 17 */ 18 19(function(Popcorn) { 20 21 var JQUERY_SCRIPT = '//code.jquery.com/jquery-1.11.1.min.js', // Used if jQuery not already present. 22 JPLAYER_SCRIPT = '//code.jplayer.org/2.9.0/jplayer/jquery.jplayer.min.js', // Used if jPlayer not already present. 23 JPLAYER_SWFPATH = '//code.jplayer.org/2.9.0/jplayer/jquery.jplayer.swf', // Used if not specified in jPlayer options via SRC Object. 24 SOLUTION = 'html,flash', // The default solution option. 25 DEBUG = false, // Decided to leave the debugging option and console output in for the time being. Overhead is trivial. 26 jQueryDownloading = false, // Flag to stop multiple instances from each pulling in jQuery, thus corrupting it. 27 jPlayerDownloading = false, // Flag to stop multiple instances from each pulling in jPlayer, thus corrupting it. 28 format = { // Duplicate of jPlayer 2.5.0 object, to avoid always requiring jQuery and jPlayer to be loaded before performing the _canPlayType() test. 29 mp3: { 30 codec: 'audio/mpeg', 31 flashCanPlay: true, 32 media: 'audio' 33 }, 34 m4a: { // AAC / MP4 35 codec: 'audio/mp4; codecs="mp4a.40.2"', 36 flashCanPlay: true, 37 media: 'audio' 38 }, 39 m3u8a: { // AAC / MP4 / Apple HLS 40 codec: 'application/vnd.apple.mpegurl; codecs="mp4a.40.2"', 41 flashCanPlay: false, 42 media: 'audio' 43 }, 44 m3ua: { // M3U 45 codec: 'audio/mpegurl', 46 flashCanPlay: false, 47 media: 'audio' 48 }, 49 oga: { // OGG 50 codec: 'audio/ogg; codecs="vorbis, opus"', 51 flashCanPlay: false, 52 media: 'audio' 53 }, 54 flac: { // FLAC 55 codec: 'audio/x-flac', 56 flashCanPlay: false, 57 media: 'audio' 58 }, 59 wav: { // PCM 60 codec: 'audio/wav; codecs="1"', 61 flashCanPlay: false, 62 media: 'audio' 63 }, 64 webma: { // WEBM 65 codec: 'audio/webm; codecs="vorbis"', 66 flashCanPlay: false, 67 media: 'audio' 68 }, 69 fla: { // FLV / F4A 70 codec: 'audio/x-flv', 71 flashCanPlay: true, 72 media: 'audio' 73 }, 74 rtmpa: { // RTMP AUDIO 75 codec: 'audio/rtmp; codecs="rtmp"', 76 flashCanPlay: true, 77 media: 'audio' 78 }, 79 m4v: { // H.264 / MP4 80 codec: 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"', 81 flashCanPlay: true, 82 media: 'video' 83 }, 84 m3u8v: { // H.264 / AAC / MP4 / Apple HLS 85 codec: 'application/vnd.apple.mpegurl; codecs="avc1.42E01E, mp4a.40.2"', 86 flashCanPlay: false, 87 media: 'video' 88 }, 89 m3uv: { // M3U 90 codec: 'audio/mpegurl', 91 flashCanPlay: false, 92 media: 'video' 93 }, 94 ogv: { // OGG 95 codec: 'video/ogg; codecs="theora, vorbis"', 96 flashCanPlay: false, 97 media: 'video' 98 }, 99 webmv: { // WEBM 100 codec: 'video/webm; codecs="vorbis, vp8"', 101 flashCanPlay: false, 102 media: 'video' 103 }, 104 flv: { // FLV / F4V 105 codec: 'video/x-flv', 106 flashCanPlay: true, 107 media: 'video' 108 }, 109 rtmpv: { // RTMP VIDEO 110 codec: 'video/rtmp; codecs="rtmp"', 111 flashCanPlay: true, 112 media: 'video' 113 } 114 }, 115 isObject = function(val) { // Basic check for Object 116 if(val && typeof val === 'object' && val.hasOwnProperty) { 117 return true; 118 } else { 119 return false; 120 } 121 }, 122 getMediaType = function(url) { // Function to gleam the media type from the URL 123 var mediaType = false; 124 if(/\.mp3$/i.test(url)) { 125 mediaType = 'mp3'; 126 } else if(/\.mp4$/i.test(url) || /\.m4v$/i.test(url)) { 127 mediaType = 'm4v'; 128 } else if(/\.m4a$/i.test(url)) { 129 mediaType = 'm4a'; 130 } else if(/\.ogg$/i.test(url) || /\.oga$/i.test(url)) { 131 mediaType = 'oga'; 132 } else if(/\.ogv$/i.test(url)) { 133 mediaType = 'ogv'; 134 } else if(/\.webm$/i.test(url)) { 135 mediaType = 'webmv'; 136 } 137 return mediaType; 138 }, 139 getSupplied = function(url) { // Function to generate a supplied option from an src object. ie., When supplied not specified. 140 var supplied = '', 141 separator = ''; 142 if(isObject(url)) { 143 // Generate supplied option from object's properties. Non-format properties would be ignored by jPlayer. Order is unpredictable. 144 for(var prop in url) { 145 if(url.hasOwnProperty(prop)) { 146 supplied += separator + prop; 147 separator = ','; 148 } 149 } 150 } 151 if(DEBUG) console.log('getSupplied(): Generated: supplied = "' + supplied + '"'); 152 return supplied; 153 }; 154 155 Popcorn.player( 'jplayer', { 156 _canPlayType: function( containerType, url ) { 157 // url : Either a String or an Object structured similar a jPlayer media object. ie., As used by setMedia in jPlayer. 158 // The url object may also contain a solution and supplied property. 159 160 // Define the src object structure here! 161 162 var cType = containerType.toLowerCase(), 163 srcObj = { 164 media:{}, 165 options:{} 166 }, 167 rVal = false, // Only a boolean false means it is not supported. 168 mediaType; 169 170 if(cType !== 'video' && cType !== 'audio') { 171 172 if(typeof url === 'string') { 173 // Check it starts with http, so the URL is absolute... Well, it is not a perfect check. 174 if(/^http.*/i.test(url)) { 175 mediaType = getMediaType(url); 176 if(mediaType) { 177 srcObj.media[mediaType] = url; 178 srcObj.options.solution = SOLUTION; 179 srcObj.options.supplied = mediaType; 180 } 181 } 182 } else { 183 srcObj = url; // Assume the url is an src object. 184 } 185 186 // Check for Object and appropriate minimum data structure. 187 if(isObject(srcObj) && isObject(srcObj.media)) { 188 189 if(!isObject(srcObj.options)) { 190 srcObj.options = {}; 191 } 192 193 if(!srcObj.options.solution) { 194 srcObj.options.solution = SOLUTION; 195 } 196 197 if(!srcObj.options.supplied) { 198 srcObj.options.supplied = getSupplied(srcObj.media); 199 } 200 201 // Figure out how jPlayer will play it. 202 // This may not work properly when both audio and video is supplied. ie., A media player. But it should return truethy and jPlayer can figure it out. 203 204 var solution = srcObj.options.solution.toLowerCase().split(","), // Create the solution array, with prority based on the order of the solution string. 205 supplied = srcObj.options.supplied.toLowerCase().split(","); // Create the supplied formats array, with prority based on the order of the supplied formats string. 206 207 for(var sol = 0; sol < solution.length; sol++) { 208 209 var solutionType = solution[sol].replace(/^\s+|\s+$/g, ""), //trim 210 checkingHtml = solutionType === 'html', 211 checkingFlash = solutionType === 'flash', 212 mediaElem; 213 214 for(var fmt = 0; fmt < supplied.length; fmt++) { 215 mediaType = supplied[fmt].replace(/^\s+|\s+$/g, ""); //trim 216 if(format[mediaType]) { // Check format is valid. 217 218 // Create an HTML5 media element for the type of media. 219 if(!mediaElem && checkingHtml) { 220 mediaElem = document.createElement(format[mediaType].media); 221 } 222 // See if the HTML5 media element can play the MIME / Codec type. 223 // Flash also returns the object if the format is playable, so it is truethy, but that html property is false. 224 // This assumes Flash is available, but that should be dealt with by jPlayer if that happens. 225 var htmlCanPlay = !!(mediaElem && mediaElem.canPlayType && mediaElem.canPlayType(format[mediaType].codec)), 226 htmlWillPlay = htmlCanPlay && checkingHtml, 227 flashWillPlay = format[mediaType].flashCanPlay && checkingFlash; 228 // The first one found will match what jPlayer uses. 229 if(htmlWillPlay || flashWillPlay) { 230 rVal = { 231 html: htmlWillPlay, 232 type: mediaType 233 }; 234 sol = solution.length; // Exit solution loop 235 fmt = supplied.length; // Exit supplied loop 236 } 237 } 238 } 239 } 240 } 241 } 242 return rVal; 243 }, 244 // _setup: function( options ) { // Warning: options is deprecated. 245 _setup: function() { 246 var media = this, 247 myPlayer, // The jQuery selector of the jPlayer element. Usually a <div> 248 jPlayerObj, // The jPlayer data instance. For performance and DRY code. 249 mediaType = 'unknown', 250 jpMedia = {}, 251 jpOptions = {}, 252 ready = false, // Used during init to override the annoying duration dependance in the track event padding during Popcorn's isReady(). ie., We is ready after loadeddata and duration can then be set real value at leisure. 253 duration = 0, // For the durationchange event with both HTML5 and Flash solutions. Used with 'ready' to keep control during the Popcorn isReady() via loadeddata event. (Duration=0 is bad.) 254 durationchangeId = null, // A timeout ID used with delayed durationchange event. (Because of the duration=NaN fudge to avoid Popcorn track event corruption.) 255 canplaythrough = false, 256 error = null, // The MediaError object. 257 258 dispatchDurationChange = function() { 259 if(ready) { 260 if(DEBUG) console.log('Dispatched event : durationchange : ' + duration); 261 media.dispatchEvent('durationchange'); 262 } else { 263 if(DEBUG) console.log('DELAYED EVENT (!ready) : durationchange : ' + duration); 264 clearTimeout(durationchangeId); // Stop multiple triggers causing multiple timeouts running in parallel. 265 durationchangeId = setTimeout(dispatchDurationChange, 250); 266 } 267 }, 268 269 jPlayerFlashEventsPatch = function() { 270 271 /* Events already supported by jPlayer Flash: 272 * loadstart 273 * loadedmetadata (M4A, M4V) 274 * progress 275 * play 276 * pause 277 * seeking 278 * seeked 279 * timeupdate 280 * ended 281 * volumechange 282 * error <- See the custom handler in jPlayerInit() 283 */ 284 285 /* Events patched: 286 * loadeddata 287 * durationchange 288 * canplaythrough 289 * playing 290 */ 291 292 /* Events NOT patched: 293 * suspend 294 * abort 295 * emptied 296 * stalled 297 * loadedmetadata (MP3) 298 * waiting 299 * canplay 300 * ratechange 301 */ 302 303 // Triggering patched events through the jPlayer Object so the events are homogeneous. ie., The contain the event.jPlayer data structure. 304 305 var checkDuration = function(event) { 306 if(event.jPlayer.status.duration !== duration) { 307 duration = event.jPlayer.status.duration; 308 dispatchDurationChange(); 309 } 310 }, 311 312 checkCanPlayThrough = function(event) { 313 if(!canplaythrough && event.jPlayer.status.seekPercent === 100) { 314 canplaythrough = true; 315 setTimeout(function() { 316 if(DEBUG) console.log('Trigger : canplaythrough'); 317 jPlayerObj._trigger($.jPlayer.event.canplaythrough); 318 }, 0); 319 } 320 }; 321 322 myPlayer.bind($.jPlayer.event.loadstart, function() { 323 setTimeout(function() { 324 if(DEBUG) console.log('Trigger : loadeddata'); 325 jPlayerObj._trigger($.jPlayer.event.loadeddata); 326 }, 0); 327 }) 328 .bind($.jPlayer.event.progress, function(event) { 329 checkDuration(event); 330 checkCanPlayThrough(event); 331 }) 332 .bind($.jPlayer.event.timeupdate, function(event) { 333 checkDuration(event); 334 checkCanPlayThrough(event); 335 }) 336 .bind($.jPlayer.event.play, function() { 337 setTimeout(function() { 338 if(DEBUG) console.log('Trigger : playing'); 339 jPlayerObj._trigger($.jPlayer.event.playing); 340 }, 0); 341 }); 342 343 if(DEBUG) console.log('Created CUSTOM event handlers for FLASH'); 344 }, 345 346 jPlayerInit = function() { 347 (function($) { 348 349 myPlayer = $('#' + media.id); 350 351 if(typeof media.src === 'string') { 352 mediaType = getMediaType(media.src); 353 jpMedia[mediaType] = media.src; 354 jpOptions.supplied = mediaType; 355 jpOptions.solution = SOLUTION; 356 } else if(isObject(media.src)) { 357 jpMedia = isObject(media.src.media) ? media.src.media : {}; 358 jpOptions = isObject(media.src.options) ? media.src.options : {}; 359 jpOptions.solution = jpOptions.solution || SOLUTION; 360 jpOptions.supplied = jpOptions.supplied || getSupplied(media.src.media); 361 } 362 363 // Allow the swfPath to be set to local server. ie., If the jPlayer Plugin is local and already on the page, then you can also use the local SWF. 364 jpOptions.swfPath = jpOptions.swfPath || JPLAYER_SWFPATH; 365 366 myPlayer.bind($.jPlayer.event.ready, function(event) { 367 if(event.jPlayer.flash.used) { 368 jPlayerFlashEventsPatch(); 369 } 370 // Set the media andd load it, so that the Flash solution behaves similar to HTML5 solution. 371 // This also allows the loadstart event to be used to know jPlayer is ready. 372 $(this).jPlayer('setMedia', jpMedia).jPlayer('load'); 373 }); 374 375 // Do not auto-bubble the reserved events, nor the loadeddata and durationchange event, since the duration must be carefully handled when loadeddata event occurs. 376 // See the duration property code for more details. (Ranting.) 377 378 var reservedEvents = $.jPlayer.reservedEvent + ' loadeddata durationchange', 379 reservedEvent = reservedEvents.split(/\s+/g); 380 381 // Generate event handlers for all the standard HTML5 media events. (Except durationchange) 382 383 var bindEvent = function(name) { 384 myPlayer.bind($.jPlayer.event[name], function(event) { 385 if(DEBUG) console.log('Dispatched event: ' + name + (event && event.jPlayer ? ' (' + event.jPlayer.status.currentTime + 's)' : '')); // Must be after dispatch for some reason on Firefox/Opera 386 media.dispatchEvent(name); 387 }); 388 if(DEBUG) console.log('Created event handler for: ' + name); 389 }; 390 391 for(var eventName in $.jPlayer.event) { 392 if($.jPlayer.event.hasOwnProperty(eventName)) { 393 var nativeEvent = true; 394 for(var iRes in reservedEvent) { 395 if(reservedEvent.hasOwnProperty(iRes)) { 396 if(reservedEvent[iRes] === eventName) { 397 nativeEvent = false; 398 break; 399 } 400 } 401 } 402 if(nativeEvent) { 403 bindEvent(eventName); 404 } else { 405 if(DEBUG) console.log('Skipped auto event handler creation for: ' + eventName); 406 } 407 } 408 } 409 410 myPlayer.bind($.jPlayer.event.loadeddata, function(event) { 411 if(DEBUG) console.log('Dispatched event: loadeddata' + (event && event.jPlayer ? ' (' + event.jPlayer.status.currentTime + 's)' : '')); 412 media.dispatchEvent('loadeddata'); 413 ready = true; 414 }); 415 if(DEBUG) console.log('Created CUSTOM event handler for: loadeddata'); 416 417 myPlayer.bind($.jPlayer.event.durationchange, function(event) { 418 duration = event.jPlayer.status.duration; 419 dispatchDurationChange(); 420 }); 421 if(DEBUG) console.log('Created CUSTOM event handler for: durationchange'); 422 423 // The error event is a special case. Plus jPlayer error event assumes it is a broken URL. (It could also be a decoder error... Or aborted or a Network error.) 424 myPlayer.bind($.jPlayer.event.error, function(event) { 425 // Not sure how to handle the error situation. Popcorn does not appear to have the error or error.code property documented here: http://popcornjs.org/popcorn-docs/media-methods/ 426 // If any error event happens, then something has gone pear shaped. 427 428 error = event.jPlayer.error; // Saving object pointer, not a copy of the object. Possible garbage collection issue... But the player is dead anyway, so don't care. 429 430 if(error.type === $.jPlayer.error.URL) { 431 error.code = 4; // MEDIA_ERR_SRC_NOT_SUPPORTED since jPlayer makes this assumption. It is the most common error, then the decode error. Never seen either of the other 2 error types occur. 432 } else { 433 error.code = 0; // It was a jPlayer error, not an HTML5 media error. 434 } 435 436 if(DEBUG) console.log('Dispatched event: error'); 437 if(DEBUG) console.dir(error); 438 media.dispatchEvent('error'); 439 }); 440 if(DEBUG) console.log('Created CUSTOM event handler for: error'); 441 442 Popcorn.player.defineProperty( media, 'error', { 443 set: function() { 444 // Read-only property 445 return error; 446 }, 447 get: function() { 448 return error; 449 } 450 }); 451 452 Popcorn.player.defineProperty( media, 'currentTime', { 453 set: function( val ) { 454 if(jPlayerObj.status.paused) { 455 myPlayer.jPlayer('pause', val); 456 } else { 457 myPlayer.jPlayer('play', val); 458 } 459 return val; 460 }, 461 get: function() { 462 return jPlayerObj.status.currentTime; 463 } 464 }); 465 466 /* The joy of duration and the loadeddata event isReady() handler 467 * The duration is assumed to be a NaN or a valid duration. 468 * jPlayer uses zero instead of a NaN and this screws up the Popcorn track event start/end arrays padding. 469 * This line here: 470 * videoDurationPlus = duration != duration ? Number.MAX_VALUE : duration + 1; 471 * Not sure why it is not simply: 472 * videoDurationPlus = Number.MAX_VALUE; // Who cares if the padding is close to the real duration? 473 * So if you trigger loadeddata before the duration is correct, the track event padding is screwed up. (It pads the start, not the end... Well, duration+1 = 0+1 = 1s) 474 * That line makes the MP3 Flash fallback difficult to setup. The whole MP3 will need to load before the duration is known. 475 * Planning on using a NaN for duration until a >0 value is found... Except with MP3, where seekPercent must be 100% before setting the duration. 476 * Why not just use a NaN during init... And then correct the duration later? 477 */ 478 479 Popcorn.player.defineProperty( media, 'duration', { 480 set: function() { 481 // Read-only property 482 if(ready) { 483 return duration; 484 } else { 485 return NaN; 486 } 487 }, 488 get: function() { 489 if(ready) { 490 return duration; // Popcorn has initialized, we can now use duration zero or whatever without fear. 491 } else { 492 return NaN; // Keep the duration a NaN until after loadeddata event has occurred. Otherwise Popcorn track event padding is corrupted. 493 } 494 } 495 }); 496 497 Popcorn.player.defineProperty( media, 'muted', { 498 set: function( val ) { 499 myPlayer.jPlayer('mute', val); 500 return jPlayerObj.options.muted; 501 }, 502 get: function() { 503 return jPlayerObj.options.muted; 504 } 505 }); 506 507 Popcorn.player.defineProperty( media, 'volume', { 508 set: function( val ) { 509 myPlayer.jPlayer('volume', val); 510 return jPlayerObj.options.volume; 511 }, 512 get: function() { 513 return jPlayerObj.options.volume; 514 } 515 }); 516 517 Popcorn.player.defineProperty( media, 'paused', { 518 set: function() { 519 // Read-only property 520 return jPlayerObj.status.paused; 521 }, 522 get: function() { 523 return jPlayerObj.status.paused; 524 } 525 }); 526 527 media.play = function() { 528 myPlayer.jPlayer('play'); 529 }; 530 media.pause = function() { 531 myPlayer.jPlayer('pause'); 532 }; 533 534 myPlayer.jPlayer(jpOptions); // Instance jPlayer. Note that the options should not have a ready event defined... Kill it by default? 535 jPlayerObj = myPlayer.data('jPlayer'); 536 537 }(jQuery)); 538 }, 539 540 jPlayerCheck = function() { 541 if (!jQuery.jPlayer) { 542 if (!jPlayerDownloading) { 543 jPlayerDownloading = true; 544 Popcorn.getScript(JPLAYER_SCRIPT, function() { 545 jPlayerDownloading = false; 546 jPlayerInit(); 547 }); 548 } else { 549 setTimeout(jPlayerCheck, 250); 550 } 551 } else { 552 jPlayerInit(); 553 } 554 }, 555 556 jQueryCheck = function() { 557 if (!window.jQuery) { 558 if (!jQueryDownloading) { 559 jQueryDownloading = true; 560 Popcorn.getScript(JQUERY_SCRIPT, function() { 561 jQueryDownloading = false; 562 jPlayerCheck(); 563 }); 564 } else { 565 setTimeout(jQueryCheck, 250); 566 } 567 } else { 568 jPlayerCheck(); 569 } 570 }; 571 572 jQueryCheck(); 573 }, 574 _teardown: function() { 575 jQuery('#' + this.id).jPlayer('destroy'); 576 } 577 }); 578 579}(Popcorn));