1/** 2* Embedding code for JWPlayer to use Dealer.com playlists 3* This gives an API to embed the player with standard jwplayer options or uses data attributes of the target element 4* The resulting videos are registered in the DDC.Video global object 5* Copyright (c) 2014 Dealer.com. 6* 7* Requires: 8* JQuery 1.5+ 9* JQuery cookie plugin (only if using preroll functionality) 10* 11* Created: 15.July.2013, Orson Bradford 12* Last modified: 3.February.2014, Orson Bradford 13*/ 14 15/*! Allow CORS header support for ajax requests in IE 16 * jQuery-ajaxTransport-XDomainRequest - v1.0.1 - 2013-10-17 17 * https://github.com/MoonScript/jQuery-ajaxTransport-XDomainRequest 18 * Copyright (c) 2013 Jason Moon (@JSONMOON) 19 * Licensed MIT (/blob/master/LICENSE.txt) 20 */ 21(function($){if(!$.support.cors&&$.ajaxTransport&&window.XDomainRequest){var n=/^https?:\/\//i;var o=/^get|post$/i;var p=new RegExp('^'+location.protocol,'i');var q=/text\/html/i;var r=/\/json/i;var s=/\/xml/i;$.ajaxTransport('* text html xml json',function(i,j,k){if(i.crossDomain&&i.async&&o.test(i.type)&&n.test(i.url)&&p.test(i.url)){var l=null;var m=(j.dataType||'').toLowerCase();return{send:function(f,g){l=new XDomainRequest();if(/^\d+$/.test(j.timeout)){l.timeout=j.timeout}l.ontimeout=function(){g(500,'timeout')};l.onload=function(){var a='Content-Length: '+l.responseText.length+'\r\nContent-Type: '+l.contentType;var b={code:200,message:'success'};var c={text:l.responseText};try{if(m==='html'||q.test(l.contentType)){c.html=l.responseText}else if(m==='json'||(m!=='text'&&r.test(l.contentType))){try{c.json=$.parseJSON(l.responseText)}catch(e){b.code=500;b.message='parseerror'}}else if(m==='xml'||(m!=='text'&&s.test(l.contentType))){var d=new ActiveXObject('Microsoft.XMLDOM');d.async=false;try{d.loadXML(l.responseText)}catch(e){d=undefined}if(!d||!d.documentElement||d.getElementsByTagName('parsererror').length){b.code=500;b.message='parseerror';throw'Invalid XML: '+l.responseText;}c.xml=d}}catch(parseMessage){throw parseMessage;}finally{g(b.code,b.message,c,a)}};l.onprogress=function(){};l.onerror=function(){g(500,'error',{text:l.responseText})};var h='';if(j.data){h=($.type(j.data)==='string')?j.data:$.param(j.data)}l.open(i.type,i.url);l.send(h)},abort:function(){if(l){l.abort()}}}}})}})(jQuery); 22 23window.DDC = window.DDC || {}; 24 25// ************************************ 26// Register as Global Module 27// ************************************ 28 29DDC.Video = (function ($) { 30 "use strict"; 31 var version = '2.14', 32 // module object to hold externally accessible methods and properties 33 module = {}, 34 supportsTouch = 'ontouchstart' in window || navigator.msMaxTouchPoints, 35 ddcHostName = "https://static.dealer.com/", 36 jwPlayerkey = 'CuRkq2mEg9sCOY9v/r3gGjfwpRHO+3kZk713oA==', 37 // Falling back to jwplayer 6.5 until the skin performance has been improved 38 jwplayerPath = ddcHostName + 'global/media/js/ddc/apis/videoplayer/v2/jwplayer/v6.5/jwplayer.js', 39 flashplayerPath = ddcHostName + 'global/media/js/ddc/apis/videoplayer/v2/jwplayer/v6.5/jwplayer.flash.swf', 40 html5playerPath = ddcHostName + 'global/media/js/ddc/apis/videoplayer/v2/jwplayer/v6.5/jwplayer.html5.js', 41 // Optional proxy for cross domain ajax requests using the DDC Request Proxy Portlet 42 // (works for playlists and captions files but not skin xml files because jwplayer can't handle proxy escape chars) 43 // proxyPath = '/proxy.htm?_minify=false&url=', 44 proxyPath = null, 45 prerollPathPattern = window.location.protocol+'//videos2.dealer.com/clients/%INIT%/%ID%/preroll.xml', 46 postrollPathPattern = window.location.protocol+'//videos2.dealer.com/clients/%INIT%/%ID%/postroll.xml', 47 skinsPath = ddcHostName + 'global/media/js/ddc/apis/videoplayer/v2/jwplayer/v6.5/skins/', 48 localeSkins = { 49 'glowddc' : { 50 'en_US' : skinsPath + 'glowddc/glowddc_en.xml', 51 'en_CA' : skinsPath + 'glowddc/glowddc_en.xml', 52 'es_US' : skinsPath + 'glowddc/glowddc_es.xml', 53 'fr_CA' : skinsPath + 'glowddc/glowddc_fr.xml' 54 } 55 }, 56 jwDefaults = { 57 primary : (isIE() && isIE() < 10) ? 'flash' : 'html5', 58 flashplayer : flashplayerPath, 59 html5player : html5playerPath, 60 width : '100%', 61 aspectratio: '4:3', 62 stretching : 'uniform', 63 skin : localeSkins.glowddc.en_US 64 }, 65 // note these wrapper defaults will take priority over data attributes of target element, so leave undefined to override 66 // keep all lowercase for normalization with element data attributes 67 // see DDCVideoWrapper definition for more details 68 defaults = { 69 target: '.ddcPlayer-holder', 70 targetid : undefined, 71 playerid : undefined, 72 wrapperelement: '<div class="ddc-video-wrapper" />', 73 accountid : undefined, 74 vehicleid : undefined, 75 videourl : undefined, 76 ddcplaylist : undefined, 77 useprepostroll : undefined, 78 prerollplaylist : undefined, 79 postrollplaylist : undefined, 80 locale : undefined, // if set, uses the appropriate version of the localeskin below, rather than from the jwsettings 81 localeskin : 'glowddc', 82 jwplaylist : [], 83 jwsettings: {} 84 }, 85 jwPlayerScriptPromise, 86 trackerSettings = { 87 ddc : { 88 viewTypeVideo : 16 89 }, 90 ga : { 91 id : 'UA-9899509-3', 92 category : 'Video' 93 } 94 }, 95 tracker = { 96 ddc : undefined, 97 ga : undefined 98 }; 99 // use absolute paths for third party (CarSales) support 100 101 102 // ************************************ 103 // DDC.Video exposed methods and properties 104 // ************************************ 105 106 module.version = version; 107 module.jwplayerPath = jwplayerPath; 108 module.proxyPath = proxyPath; 109 module.localeSkins = localeSkins; 110 module.jwDefaults = jwDefaults; 111 module.wrappers = []; 112 113 // hoisted from function definitions below 114 module.init = initEachWrapper; 115 module.embed = embedEachWrapper; 116 module.onReady = onReadyEachWrapper; 117 module.getWrapper = getWrapper; 118 module.getAllWrappers = getAllWrappers; 119 module.getPlayer = getPlayer; 120 module.getAllPlayers = getAllPlayers; 121 module.getAllPlayerIds = getAllPlayerIds; 122 123 /** 124 * Remove all players cleanly when unloading page 125 * Chrome has buffering errors JIRA-31593 126 */ 127 window.onbeforeunload = function () { 128 var allplayers = []; 129 try { 130 allplayers = DDC.Video.getAllPlayers(); 131 132 for (var i = 0; i < allplayers.length; i++) { 133 allplayers[i].remove(); 134 } 135 } catch (e) { 136 if (window.console && window.console.log) { 137 console.log('DDC.Video unable to clean up players.'); 138 } 139 } 140 }; 141 142 /** 143 * Simple IE check 144 */ 145 function isIE() { 146 var userAgent = navigator.userAgent.toLowerCase(); 147 return (userAgent.indexOf('msie') != -1) ? parseInt(userAgent.
147split('msie')[1], 10) : false; 148 } 149 150 /** 151 * Ajax Load JWPlayer Scripts 152 */ 153 function getJWPlayerScript() { 154 var deferred = $.Deferred(); 155 156 // Reference for handlers which depend on jwplayer being available 157 jwPlayerScriptPromise = deferred.promise(); 158 159 $.ajax({url: DDC.Video.jwplayerPath, 160 dataType: 'script', 161 cache: true 162 }).fail(function (jqXHR, textStatus, errorThrown) { 163 if (window.console && window.console.log) { 164 console.log('Videoplayer unable to load jwplayer script'); 165 } 166 deferred.reject(); 167 }).done(function (jqXHR, textStatus) { 168 window.jwplayer.key = jwPlayerkey; 169 deferred.resolve(); 170 }); 171 } 172 173 /** 174 * Constructor for the wrapper object 175 * 176 * @param Object options An object literal containing key/value pairs used by the Dealer.com JWPlayer wrapper. 177 * @option mixed target A jQuery selector or jQuery object reference of the target element (which will contain the video). 178 * @option string targetid Id for the element which will contain the player ('target' above takes precedence). 179 * @option string playerid id for the player 180 * @option string wrapperelement HTML string to wrap video player with (since jwPlayer API gives no control over internal wrapper elements) 181 * @option string accountid DDC account id used to get pre/post roll paths 182 * @option string vehicleid DDC vehicle id (currently unused, but included from legacy v8 player) 183 * @option string ddcplaylist Path to a Dealer.com XML playlist file listing video assests for video player. 184 * @option string useprepostroll Flag to indicate whether to try to load pre/post roll playlists. 185 * @option string prerollplaylist Path to a Preroll XML playlist file listing video assests for video player. 186 * @option string postrollplaylist Path to a Postroll XML playlist file listing video assests for video player. 187 * @option string videourl direct URL for a video file; only used if a DDC playlist is not provided. 188 * @option string locale language for text controls: en_US, es_US, en_CA, fr_CA 189 * @option Object jwplaylist A jwPlayer playlist object which will get used in the jwsettings at embed time 190 * @option Object jwsettings A jwPlayer options object (http://www.longtailvideo.com/support/jw-player/28839/embedding-the-player/) 191 */ 192 var DDCVideoWrapper = function (options) { 193 this.options = options; 194 this.playlistPromise = undefined; 195 this.mainPlaylistPromise = undefined; 196 this.prerollPlaylistPromise = undefined; 197 this.postrollPlaylistPromise = undefined; 198 this.hasPreroll = undefined; 199 this.jwPlayerInstance = undefined; 200 this.playerState = 'created'; // change based on jwPlayer ready state. Either 'created', 'initializing', 'ready' 201 }; 202 203 DDCVideoWrapper.prototype.init = init; 204 DDCVideoWrapper.prototype.embed = embed; 205 DDCVideoWrapper.prototype.onEmbed = onEmbed; 206 DDCVideoWrapper.prototype.onReady = onReady; 207 208 /** 209 * Constructor for collection of wrappers created as a batch 210 * 211 * This maps the basic methods 'init', 'embed', 'onReady' to versions of these functions 212 * which iterate over each wrapper in the collection. 'doFnEachWrapper' is a generic function which gets used to do this. 213 * 214 * @param Object options (See DDCVideoWrapper) shared in all wrappers in this collection 215 */ 216 var DDCVideoWrapperCollection = function (options) { 217 this.wrappers = []; 218 this.options = options; 219 }; 220 221 DDCVideoWrapperCollection.prototype.addWrapper = addWrapper; 222 DDCVideoWrapperCollection.prototype.getWrapper = getWrapper; 223 DDCVideoWrapperCollection.prototype.init = initEachWrapper; 224 DDCVideoWrapperCollection.prototype.embed = embedEachWrapper; 225 DDCVideoWrapperCollection.prototype.onReady = onReadyEachWrapper; 226 DDCVideoWrapperCollection.prototype.doFnEachWrapper = doFnEachWrapper; 227 228 /** 229 * Put a wrapper into the collection 230 * (either global Video context or for given DDCVideoWrapperCollection) 231 * 232 * @param DDCVideoWrapper wrapperObj wrapper to add 233 */ 234 function addWrapper(wrapperObj) { 235 var wrapperAdded; 236 237 if (wrapperObj instanceof DDCVideoWrapper && !this.getWrapper(wrapperObj.options.targetid)) { 238 this.wrappers.push(wrapperObj); 239 wrapperAdded = wrapperObj; 240 } 241 242 return wrapperAdded; 243 } 244 245 /** 246 * Get a wrapper from the collection by its target id, the player id or a jquery reference 247 * (either in global DDC.Video context or for given DDCVideoWrapperCollection) 248 * 249 * @param mixed reference 250 */ 251 function getWrapper(reference) { 252 253 var refId, 254 wrapper; 255 256 if (typeof reference === 'string') { 257 refId = reference; 258 } else if (reference instanceof jQuery) { 259 refId = reference.attr('id'); 260 } 261 262 if (reference) { 263 for (var i = this.wrappers.length - 1; i >= 0; i--) { 264 if ((this.wrappers[i].options.targetid === refId) || (this.wrappers[i].options.playerid === refId)) { 265 wrapper = this.wrappers[i]; 266 break; 267 } 268 } 269 } 270 271 return wrapper; 272 } 273 274 /** 275 * Get array of all wrappers registered globally 276 * 277 */ 278 function getAllWrappers() { 279 280 return this.wrappers; 281 } 282 283 /** 284 * Get a jwplayer instance from id 285 * (either global Video context or for given DDCVideoWrapperCollection) 286 * 287 * @param mixed reference 288 */ 289 function getPlayer(reference) { 290 var refId, 291 wrapper, 292 player; 293 294 if (typeof reference === 'string') { 295 refId = reference; 296 } else if (reference instanceof jQuery) { 297 refId = reference.attr('id'); 298 } 299 300 wrapper = this.getWrapper(reference); 301 if (wrapper) { 302 player = wrapper.jwPlayerInstance; 303 } 304 305 // fallback to jwplayer built-in method 306 if (!player && window.jwplayer) { 307 player = window.jwplayer(refId); 308 } 309 310 return player; 311 } 312 313 /** 314 * Get array of all jwplayer instances registered globally 315 * 316 */ 317 function getAllPlayers() { 318 var players = []; 319 for (var i = this.wrappers.length - 1; i >= 0; i--) { 320 if (this.wrappers[i].jwPlayerInstance) { 321 players.push(this.wrappers[i].jwPlayerInstance); 322 } 323 } 324 325 return players; 326 } 327 328 /** 329 * Get array of all jwplayer ids registered globally 330 * 331 */ 332 function getAllPlayerIds() { 333 var playerIds = []; 334 for (var i = this.wrappers.length - 1; i >= 0; i--) { 335 playerIds.push(this.wrappers[i].options.playerid); 336 } 337 338 return playerIds; 339 } 340 341 /** 342 * Wrapper function which routes init method through doFnEachWrapper 343 * 344 * @param Object options (See DDCVideoWrapper) 345 * @return DDCVideoWrapperCollection 346 */ 347 function initEachWrapper(options) {
348 // ************************************ 349 // Init Each Wrapper 350 // ************************************ 351 return doFnEachWrapper.call(this, init, options); 352 } 353 354 /** 355 * Wrapper function which routes embed method through doFnEachWrapper 356 * 357 * @param Object options (See DDCVideoWrapper) 358 * @return DDCVideoWrapperCollection 359 */ 360 function embedEachWrapper(options) { 361 // ************************************ 362 // Embed Each Wrapper 363 // ************************************ 364 return doFnEachWrapper.call(this, embed, options); 365 } 366 367 /** 368 * This is an abstraction to allow for some reuse between initEachWrapper() and embedEachWrapper() 369 * If these two functions diverge, the stubs above could become distinct methods 370 * 371 * @param Function fn A DDCVideoWrapper method which to call on each wrapper instance 372 * @param Object options (See DDCVideoWrapper) 373 * @return DDCVideoWrapperCollection current collection, or a new one if not set 374 */ 375 function doFnEachWrapper(fn, options) { 376 var collection, 377 $target; 378 379 // Normalize fn to function reference, if passed as a string 380 if (typeof fn === 'string') { 381 if (DDCVideoWrapper.prototype[fn] && typeof DDCVideoWrapper.prototype[fn] === 'function') { 382 fn = DDCVideoWrapper.prototype[fn]; 383 } 384 } 385 386 // Get jwPlayer script if not already loaded 387 if (!window.jwplayer) { 388 getJWPlayerScript(); 389 } 390 391 // ************************************ 392 // Set up Tracking 393 // ************************************ 394 395 if (!tracker.ga && !tracker.ddc) { 396 tracker = configureTracker(tracker, trackerSettings); 397 } 398 399 // ************************************ 400 // Instantiate Collection Object 401 // ************************************ 402 403 if (this instanceof DDCVideoWrapperCollection) { 404 collection = this; 405 collection.options = mergeOptions(collection.options, options); 406 } else { 407 // populate with defaults 408 options = mergeOptions(defaults, options); 409 collection = new DDCVideoWrapperCollection(options); 410 } 411 412 // ************************************ 413 // Acquire $target 414 // ************************************ 415 416 $target = acquireTarget(collection.options); 417 418 // ************************************ 419 // Delegate Fn calls per target 420 // (init or embed) 421 // ************************************ 422 423 if ($target && $target.length > 0) { 424 $target.each(function (i, targetEl) { 425 var itemWrapperInstance, 426 itemOptions = $.extend(true, {}, collection.options), 427 $itemTarget = $(targetEl), 428 // 0 indexed counter for the player Ids: ddc-jwplayer-0, ... 429 defaultPlayerId = ('ddc-jwplayer-' + DDC.Video.wrappers.length); 430 431 // If not set, retrieve or generate unique targetid 432 433 if (!itemOptions.targetid) { 434 if ($itemTarget.attr('id')) { 435 itemOptions.targetid = $itemTarget.attr('id'); 436 } else { 437 itemOptions.targetid = defaultPlayerId + '-target'; 438 $itemTarget.attr('id', itemOptions.targetid); 439 } 440 } 441 442 // Reset the existing 'target' to be this specific unique element - otherwise the generic selector will remain on these options and it will pick up multiple targets, in the individual video init calls 443 itemOptions.target = $itemTarget; 444 445 // Override an existing wrapper object, if there is one 446 itemWrapperInstance = DDC.Video.getWrapper(itemOptions.targetid); 447 448 // Or create a new one 449 if (!itemWrapperInstance) { 450 451 if (!itemOptions.playerid) { 452 itemOptions.playerid = defaultPlayerId; 453 } 454 455 itemWrapperInstance = new DDCVideoWrapper(itemOptions); 456 collection.addWrapper(itemWrapperInstance); 457 // add to global collection as well. method isn't exposed on DDC.Video, so using call() 458 addWrapper.call(DDC.Video, itemWrapperInstance); 459 } 460 461 // Try to call argument 'fn' (init or embed) on the wrapper instance 462 if (typeof fn === "function") { 463 try { 464 fn.call(itemWrapperInstance, itemOptions); 465 } catch (e) { 466 if (window.console && window.console.log) { 467 console.log('Not able to run function on wrapper'); 468 } 469 } 470 } 471 }); 472 } else { 473 // No targets. May not exist in DOM yet on init, and could exist later when call embed. 474 // TODO - to be most efficient, playlist ajax calls should be made on init even without $target as long as playlist paths are defined in options (as opposed to in data attribs) 475 } 476 477 return collection; 478 } 479 480 /** 481 * Unlike previous this is NOT a wrapper to doFnEachWrapper 482 * It passes a function which is run on each wrapper's onReady callback 483 * So the callback function will repeat once per wrapper 484 * 485 * @param Function fn A user-defined callback function 486 * @return DDCVideoWrapperCollection 487 */ 488 function onReadyEachWrapper(fn) { 489 var collection; 490 491 // ************************************ 492 // Instantiate Collection Object
493 // ************************************ 494 495 if (this instanceof DDCVideoWrapperCollection) { 496 collection = this; 497 } else { 498 collection = new DDCVideoWrapperCollection(); 499 collection.wrappers = []; 500 collection.init(); 501 } 502 503 for (var i = this.wrappers.length - 1; i >= 0; i--) { 504 if (collection.wrappers[i] instanceof DDCVideoWrapper) { 505 collection.wrappers[i].onEmbedReadyCallback = fn; 506 } 507 } 508 509 return collection; 510 } 511 512 /** 513 * Extend "Base Options" (usually either collection level options or defaults) with more specific player options 514 * 515 * @param Object baseOptions (See DDCVideoWrapper) 516 * @param Object options (See DDCVideoWrapper) 517 * @return Object merged clone of options merged with base group options 518 */ 519 function mergeOptions(baseOptions, options) { 520 var merged = {}; 521 522 if (baseOptions && typeof baseOptions === "object") { 523 baseOptions = normalizeOptions(baseOptions); 524 } else { 525 baseOptions = {}; 526 } 527 528 if (options && typeof options === "object") { 529 options = normalizeOptions(options); 530 } else { 531 options = {}; 532 } 533 534 // deep extend the remaining options, so as to properly merge nested jwsettings, if any 535 $.extend(true, merged, baseOptions, options); 536 537 return merged; 538 } 539 540 /** 541 * Lowercase all option names to disambiguate and simplify merging 542 * Also catch certain synonymous option names 543 * 544 * @param Object options (See DDCVideoWrapper) 545 * @return Object normOptions normalized values 546 */ 547 function normalizeOptions(options) { 548 var normOptions = {}, 549 normOption; 550 551 for (var option in options) { 552 if (options.hasOwnProperty(option)) { 553 // normalize case - all options are lowercase 554 normOption = option.toString().toLowerCase(); 555 556 // trap naming variants 557 switch (normOption) { 558 559 case 'autoplay': 560 if (options[option] === true || options[option] === 'true') { 561 normOptions.autostart = true; 562 } 563 break; 564 565 case 'divname': 566 normOptions.targetid = options[option]; 567 break; 568 569 case 'height': 570 case 'width': 571 // specify without 'px' units, but '%' is allowed 572 normOptions[normOption] = (options[option].toString().indexOf('%') > 0) ? parseInt(options[option], 10) + '%' : parseInt(options[option], 10).toString(); 573 break; 574 575 case 'ddcplaylist': 576 case 'ddcplaylisturl': 577 case 'playlistxmlpath': 578 normOptions.ddcplaylist = getProxiedPath(options[option]); 579 break; 580 581 case 'postrollxmlpath': 582 normOptions.postrollplaylist = getProxiedPath(options[option]); 583 break; 584 585 case 'prerollxmlpath': 586 normOptions.prerollplaylist = getProxiedPath(options[option]); 587 break; 588 589 case 'showcontrols': 590 normOptions.controls = options[option]; 591 break; 592 593 case 'allowfullscreen': 594 case 'calltoactionpath':
595 case 'overlaypath': 596 case 'showplaylist': 597 case 'swfid': 598 //unused legacy options 599 break; 600 601 default: 602 normOptions[normOption] = options[option]; 603 break; 604 } 605 } 606 } 607 608 return normOptions; 609 } 610 611 /** 612 * Look up label translations 613 * 614 * @param String label 615 * @return String translated 616 */ 617 var getLabel = function (label) { 618 var i18n = (window.DDC.i18n || {}), 619 translated = label; 620 621 if (i18n.getLabel) { 622 translated = DDC.i18n.getLabel(label); 623 } 624 625 if (!translated) { 626 translated = label; 627 } 628 629 return translated; 630 }; 631 632 /** 633 * Tease out jwPlayer settings from wrapper options 634 * With fallback to jwDefaults 635 * 636 * @param Object options (See DDCVideoWrapper) 637 * @param Object jwDefaults a jwPlayer settings object 638 * @return Object jwSettings a jwPlayer settings object 639 */ 640 function getJWSettings(options, jwDefaults) { 641 var jwSettings = {}, 642 localeSkins; 643 644 // Set the exact skin file for the locale, if available 645 // Proxied skin files won't get handled properly by the jwplayer, so not attemptin to go through proxy 646 if (options.locale && options.localeskin) { 647 // check if the localeSkins have been overridden 648 localeSkins = DDC.Video.localeSkins; 649 if (localeSkins.hasOwnProperty(options.localeskin) && localeSkins[options.localeskin].hasOwnProperty(options.locale)) { 650 options.jwsettings.skin = localeSkins[options.localeskin][options.locale]; 651 } 652 } 653 654 // Transfer any wrapper options which apply into the jwplayer settings 655 for (var option in options) { 656 if (option === 'jwsettings') { 657 $.extend(true, jwSettings, options.jwsettings); 658 } else if (option === 'jwplaylist') { 659 jwSettings.playlist = options.jwplaylist; 660 } else { 661 // if option not defined in the defaults, assume we are dealing with a jwPlayer setting and grab it 662 if (options.hasOwnProperty(option) && !defaults.hasOwnProperty(option)) { 663 jwSettings[option] = options[option]; 664 } 665 } 666 } 667 668 jwSettings = $.extend(true, {}, jwDefaults, jwSettings); 669 670 return jwSettings; 671 } 672 673 /** 674 * Find the DOM element to attach the video player to 675 * 676 * @param Object options (See DDCVideoWrapper) 677 * @param Object jwPlayerInstance jwPlayer object 678 * @return jQuery $target 679 */ 680 function acquireTarget(options, wrapperInstance) { 681 var $target = $(), 682 target, 683 targetid, 684 alphaRegex = /[A-Za-z]/; 685 686 if (typeof options == "object") { 687 target = options.target; 688 targetid = options.targetid; 689 690 // if instance already exists, target it 691 if (wrapperInstance && wrapperInstance.jwPlayerInstance) { 692 if (wrapperInstance.options.target instanceof jQuery) { 693 $target = wrapperInstance.options.target; 694 } else { 695 // can't target existing video player 696 } 697 } else if (target instanceof jQuery) { 698 // param is already jQuery reference 699 $target = target; 700 } else if (targetid) { 701 // jwPlayer html5 mode is strict about invalid ids, and will not embed if it's an invalid id 702 if (alphaRegex.test(targetid[0])) { 703 $target = $('#' + targetid); 704 } else { 705 // invalid element id 706 } 707 } else if (target) { 708 // try to select element via jQuery 709 $target = $(target); 710 } else { 711 if (window.console && window.console.log) { 712 console.log('Videoplayer unable to find embed target.'); 713 } 714 } 715 } 716 717 return $target; 718 } 719 720 /** 721 * Take options and load playlist files to prepare video for embedding. 722 * 723 * @param Object options See DDCVideoWrapper 724 * @return DDCVideoWrapper 725 */ 726 function init(options) { 727 // return object is a wrapper instance (for chaining methods) 728 var wrapperInstance, 729 $target, 730 simpleFormatObj = {'file' : undefined}, 731 prerollPlaylist, 732 mainPlaylist, 733 postrollPlaylist; 734 735 // ************************************ 736 // Instantiate Wrapper Object 737 // ************************************ 738 739 if (this instanceof DDCVideoWrapperCollection) { 740 this.initAll(options); 741 wrapperInstance = null; 742 } else if (this instanceof DDCVideoWrapper) { 743 wrapperInstance = this; 744 wrapperInstance.options = mergeOptions(this.options, options); 745 } else { 746 // New instance, use the DDCVideoWrapper definition and register in the outer scope list 747 wrapperInstance = new DDCVideoWrapper(options); 748 } 749 750 if (wrapperInstance) { 751 // ************************************ 752 // Acquire $target 753 // ************************************ 754 755 // Note that it is not critical that the target element be available at init, only at embed 756 // But $target data attribute may hold the video paths extracted by buildPlaylist, so need to check for it 757 $target = acquireTarget(wrapperInstance.options, wrapperInstance); 758 759 // ************************************ 760 // Get Any Target Options from Data Attr 761 // ************************************ 762 763 // sanity check, jwplayer can only have one target 764 if ($target && $target.length == 1) { 765 // nested mergeOptions statement to ensure proper overrides: manual options > data attributes > wrapper defaults 766 wrapperInstance.options = mergeOptions(wrapperInstance.options, mergeOptions(parseTargetVideoOptions($target), options)); 767 } 768 769 // ************************************ 770 // Get playlists 771 // ************************************ 772 773 if (wrapperInstance.options.videourl) { 774 775 simpleFormatObj.file = wrapperInstance.options.videourl; 776 777 wrapperInstance.options.jwplaylist.push(simpleFormatObj); 778 779 } else if (!wrapperInstance.playlistPromise) { 780 // make sure we haven't already requested the playlists (in case of rapid repeat init calls) 781 782 // MAIN PLAYLIST 783 wrapperInstance.mainPlaylistPromise = loadPlaylist(wrapperInstance.options.ddcplaylist, wrapperInstance.options.locale); 784 785 // PRE / POST ROLL PLAYLISTS 786 if (wrapperInstance.options.useprepostroll || wrapperInstance.options.postrollplaylist || wrapperInstance.options.prerollplaylist) { 787 788 if (!wrapperInstance.options.prerollplaylist) { 789 prerollPlaylist = getProxiedPath(prerollPathPattern.replace('%INIT%', wrapperInstance.options.accountid.slice(0, 1)).replace('%ID%', wrapperInstance.options.accountid)); 790 // for example http://videos2.dealer.com/clients/d/ddcdemo/preroll.xml 791 } else { 792 prerollPlaylist = getProxiedPath(wrapperInstance.options.prerollplaylist); 793 } 794 795 if (!wrapperInstance.options.postrollplaylist) { 796 postrollPlaylist = getProxiedPath(postrollPathPattern.replace('%INIT%', wrapperInstance.options.accountid.slice(0, 1)).replace('%ID%', wrapperInstance.options.accountid)); 797 // for example http://videos2.dealer.com/clients/d/ddcdemo/postroll.xml 798 } else { 799 postrollPlaylist = getProxiedPath(wrapperInstance.options.postrollplaylist); 800 } 801 802 // PREROLL PLAYLIST
803 if ($.cookie && $.cookie('prerollViewed')) { 804 wrapperInstance.prerollPlaylistPromise = $.Deferred().resolve(); 805 } else { 806 wrapperInstance.prerollPlaylistPromise = loadPlaylist(prerollPlaylist, wrapperInstance.options.locale, simpleFormatObj); 807 } 808 809 // POSTROLL PLAYLIST 810 wrapperInstance.postrollPlaylistPromise = loadPlaylist(postrollPlaylist, wrapperInstance.options.locale, simpleFormatObj); 811 812 // When all the promises resolve 813 wrapperInstance.playlistPromise = $.when(wrapperInstance.prerollPlaylistPromise, wrapperInstance.mainPlaylistPromise, wrapperInstance.postrollPlaylistPromise).done(function (preVideoList, mainVideoList, postVideoList) { 814 var playlist = []; 815 if (mainVideoList && mainVideoList.file) { 816 if (preVideoList && preVideoList.file) { 817 preVideoList.image = mainVideoList.image; 818 wrapperInstance.hasPreroll = true; 819 playlist.push(preVideoList); 820 } 821 822 playlist.push(mainVideoList); 823 824 if (postVideoList && postVideoList.file) { 825 playlist.push(postVideoList); 826 } 827 } 828 wrapperInstance.options.jwplaylist = playlist; 829 }); 830 831 } else { 832 wrapperInstance.playlistPromise = $.when(wrapperInstance.mainPlaylistPromise).done(function (mainVideoList) { 833 var playlist = []; 834 if (mainVideoList && mainVideoList.file) { 835 playlist.push(mainVideoList); 836 wrapperInstance.options.jwplaylist = playlist; 837 } 838 }); 839 } 840 } // end if playlistPromise defined 841 } // end if wrapperInstance 842 843 return wrapperInstance; 844 } 845 846 /** 847 * Utility function to get the path using the proxyPath config (or not, if on same domain) 848 * 849 * @param String path path to transform 850 * @return String proxiedPath 851 */ 852 function getProxiedPath(path) { 853 var proxiedPath; 854 855 if (typeof path == 'string') { 856 if ((path.indexOf('http:') !== 0 && path.indexOf('https:') !== 0) || (path.indexOf(window.location.hostname.toString()) === 0) || !DDC.Video.proxyPath) { 857 // no need to proxy if on same domain 858 proxiedPath = path; 859 } else { 860 proxiedPath = DDC.Video.proxyPath + encodeURIComponent(path); 861 } 862 863 // videos.dealer.com CDN requires the %20 escape, rather than '+' separator 864 proxiedPath = proxiedPath.replace(/\+/g, '%20'); 865 } 866 867 return proxiedPath; 868 } 869 870 /** 871 * Utility function to get the original path back out of a proxiedPath 872 * 873 * @param String path path to transform 874 * @param String path proxyPath proxy path to remove from the main path arg 875 * @return String proxiedPath 876 */ 877 function getOriginalPath(path) { 878 var originalPath; 879 880 if (typeof path == 'string') { 881 if (path.indexOf(DDC.Video.proxyPath) === 0) { 882 originalPath = decodeURIComponent(path.substring(DDC.Video.proxyPath.length, path.length + 1)); 883 } else { 884 originalPath = path; 885 } 886 887 // videos.dealer.com CDN requires the %20 escape, rather than '+' separator 888 originalPath = originalPath.replace(/\+/g, '%20'); 889 } 890 891 return originalPath; 892 } 893 894 /** 895 * Given an element, try to get the video options out of its attributes 896 * Normalize the options to account for naming variation across systems 897 * 898 * @param Object $target jQuery element 899 * @return Object normOptions 900 */ 901 function parseTargetVideoOptions($target) { 902 var attr, 903 data = {}, 904 accountId, 905 autoplay, 906 ddcPlayerInfo, 907 ddcPlayerInfoArray, 908 normOptions = {}, 909 normOption; 910 911 // ************************************ 912 // Raw Attributes 913 // ************************************ 914 915 ddcPlayerInfo = $target.attr('ddcplayerinfo'); 916 917 // used to help parse the ddcplayerinfo dash separated values 918 accountId = $target.attr('hyphenatedaccountId'); 919 920 // used to fix V8 auto play issue 921 autoplay = $target.attr('autoplay'); 922 923 // ************************************ 924 // Data Attributes 925 // ************************************ 926 927 data = $target.data(); 928 929 normOptions = normalizeOptions(data); 930 931 accountId = accountId || normOptions.accountid; 932 ddcPlayerInfo = ddcPlayerInfo || normOptions.ddcplayerinfo; 933 normOptions.autoplay = autoplay || normOptions.autoplay; 934 935 // data-ddcplayerinfo has an internal structure which needs to be parsed: 936 // primary video information, of form: "[ddcplaylist]-[width]-[height]" 937 if (ddcPlayerInfo) { 938 if (accountId) { 939 ddcPlayerInfo = ddcPlayerInfo.replace(accountId, "<siteId>"); 940 } 941 942 ddcPlayerInfoArray = ddcPlayerInfo.split('-'); 943 944 if (ddcPlayerInfoArray[0]) { 945 normOptions.ddcplaylist = (accountId) ? ddcPlayerInfoArray[0].replace("<siteId>", accountId) : ddcPlayerInfoArray[0]; 946 // ddc playlist info is URI Encoded 947 normOptions.ddcplaylist = getProxiedPath(decodeURIComponent(normOptions.ddcplaylist)); 948 } 949 950 normOptions.width = $target.get(0).style.width || ddcPlayerInfoArray[1]; 951 normOptions.height = $target.get(0).style.height || ddcPlayerInfoArray[2]; 952 } 953 954 return normOptions; 955 } 956 957 /** 958 * Make a call to the appropriate playlistUrl, load the xml file and convert it to the playlist format jwPlayer uses 959 * 960 * @param string playlistUrl path to the desired playlist xml 961 * @param string locale which locale to use for UI translation 962 * @param Object formatObj a customized playlist format object to use instead of the default format 963 * @return Promise playlistPromise jQuery deferred promise object 964 */ 965 function loadPlaylist(playlistUrl, locale, formatObj) { 966 var deferred = $.Deferred(), 967 playlistPromise = deferred.promise(); 968 969 if (playlistUrl) { 970 // Ajax load the xml playlist & convert to jwPlayer playlist format 971 // dataType 'text' because need to strip leading whitespace in xml file and custom parse to JSON 972 // the default xml converters fail (due to whitespace issues?) 973 $.ajax({ 974 url: ("http:" == playlistUrl.slice(0, 5) ? (window.location.protocol + playlistUrl.slice(5)) : playlistUrl), 975 dataType: 'text' 976 977 }).fail(function (jqXHR, textStatus, errorThrown) { 978 if (window.console && window.console.log) { 979 console.log('Videoplayer unable to load playlist ' + playlistUrl + ' (' + getOriginalPath(playlistUrl) + '): ' + errorThrown); 980 } 981 }).complete(function (jqXHR, textStatus) { 982 var convertedPlaylist; 983 if (textStatus == "success") { 984 convertedPlaylist = convertDDCPlaylist(jqXHR.responseText, playlistUrl, locale, formatObj); 985 deferred.resolve(convertedPlaylist); 986 } else { 987 // failed, but still allow the promise to resolve because we don't want a failed preroll or postroll to prevent the main video from playing 988 deferred.resolve(); 989 } 990 }); 991 } 992 993 return playlistPromise; 994 } 995 996 /** 997 * Take plain text return from playlist load and try to convert to XML and parse items 998 *
999 * @param string data Raw xml text from ajax request 1000 * @param string playlistUrl path used to determine the asset path and switch betwen DDC and Carsales playlist formats 1001 * @param Object formatObj a customized playlist format object to use instead of the default format 1002 * @return Object playlist for jwPlayer options 1003 */ 1004 function convertDDCPlaylist(data, playlistUrl, locale, formatObj) { 1005 var dataXML, 1006 assetPath, 1007 vidSeg, 1008 vidLoSource, 1009 vidHiSource, 1010 vidSource, 1011 vidThumb, 1012 vidCaptionSrt, 1013 labelHighQuality = (DDC.i18n) ? getLabel("VIDEOPLAYER_QUALITY_HIGH") : "High", 1014 labelLowQuality = (DDC.i18n) ? getLabel("VIDEOPLAYER_QUALITY_LOW") : "Low", 1015 labelCaptionOn = (DDC.i18n) ? getLabel("VIDEOPLAYER_CAPTIONS_ON") : "On", 1016 format = formatObj || { 1017 file: vidSource, 1018 image: vidThumb, 1019 sources: [{ 1020 file: vidHiSource, 1021 label: labelHighQuality 1022 }, 1023 { 1024 file: vidLoSource, 1025 label: labelLowQuality 1026 }], 1027 tracks: [{ 1028 file: vidCaptionSrt, 1029 label: labelCaptionOn, 1030 kind: "captions" 1031 }] 1032 }; 1033 1034 // jwPlayer playlist to return 1035 var playlist = {}; 1036 1037 try { 1038 //strip top and bottom whitespace - don't use trim() need to support IE7/IE8 1039 dataXML = $.parseXML(data.replace(/^\s+|\s+$/g, '')); 1040 } catch (e) { 1041 if (window.console && window.console.log) { 1042 console.log('Videoplayer error reading XML file', e, data); 1043 } 1044 dataXML = null; 1045 } 1046 vidSeg = $(dataXML).find('segment'); 1047 1048 // ************************************ 1049 // Parse XML Playlist 1050 // ************************************ 1051 1052 // The playlist XML structure is different, based on CarSales versus DDC 1053 if (playlistUrl.indexOf('vidauth.au') > -1) { 1054 // CarSales Playlist structure - simple de-entitize 1055 if (vidSeg.length > 0) { 1056 vidHiSource = vidSeg.find('h264HighSource').text().replace(/&/g, '&'); 1057 vidLoSource = vidSeg.find('h264LowSource').text().replace(/&/g, '&'); 1058 vidSource = vidHiSource; 1059 vidThumb = vidSeg.find('largeThumb').text().replace(/&/g, '&'); 1060 vidCaptionSrt = ''; // no captions for Carsales format playlists 1061 } 1062 } else { 1063 // DDC default Playlist structure 1064 // get the absolute path to serve the media, since paths not specified in some (virtualTour) playlists 1065 1066 assetPath = getOriginalPath(playlistUrl); 1067 assetPath = assetPath.substring(0, assetPath.lastIndexOf("/") + 1); 1068 1069 if (vidSeg.length > 0) { 1070 1071 vidHiSource = vidSeg.attr('h264_high_src') || vidSeg.attr('flv_high_src') || ''; 1072 if (vidHiSource && vidHiSource.indexOf('http:') !== 0 && vidHiSource.indexOf('https:') !== 0) { vidHiSource = assetPath + vidHiSource; } 1073 1074 vidLoSource = vidSeg.attr('h264_low_src') || vidSeg.attr('flv_low_src') || ''; 1075 if (vidLoSource && vidLoSource.indexOf('http:') !== 0 && vidLoSource.indexOf('https:') !== 0) { vidLoSource = assetPath + vidLoSource; } 1076 1077 vidSource = vidHiSource || vidLoSource || undefined; 1078 1079 vidThumb = vidSeg.attr('lrgThumb') || ''; 1080 if (vidThumb && vidThumb.indexOf('http:') !== 0 && vidThumb.indexOf('https:') !== 0) { vidThumb = assetPath + vidThumb; } 1081 1082 vidCaptionSrt = vidSeg.attr('srt') || ''; 1083 // proxy required for captions file 1084 if (vidCaptionSrt) { 1085 if (vidCaptionSrt.indexOf('http:') !== 0 && vidCaptionSrt.indexOf('https:') !== 0) { 1086 vidCaptionSrt = getProxiedPath(assetPath + vidCaptionSrt); 1087 } else { 1088 vidCaptionSrt = getProxiedPath(vidCaptionSrt); 1089 } 1090 } else { 1091 // no captions found 1092 } 1093 } 1094 } 1095 1096 // ************************************ 1097 // Construct jwPlayer Playlist 1098 // Based on format object -- roughly 1099 // TODO Could be fine-tuned for custom formats 1100 // ************************************ 1101 1102 if (vidSource && format.hasOwnProperty('file')) { 1103 playlist.file = vidSource; 1104 } 1105 if (vidThumb && format.hasOwnProperty('image')) { 1106 playlist.image = vidThumb; 1107 } 1108 if (vidHiSource && vidLoSource && format.hasOwnProperty('sources')) { 1109 playlist.sources = [{ 1110 file: vidHiSource, 1111 label: labelHighQuality 1112 }, 1113 { 1114 file: vidLoSource, 1115 label: labelLowQuality 1116 }]; 1117 } 1118 if (vidCaptionSrt && format.hasOwnProperty('tracks')) { 1119 playlist.tracks = [{ 1120 file: vidCaptionSrt, 1121 label: labelCaptionOn, 1122 kind: "captions" 1123 }]; 1124 } 1125 1126 return playlist; 1127 } 1128 1129 /** 1130 * embed function to instantiate the video player 1131 * (will call init automatically if needed) 1132 * 1133 * @param Object options See DDCVideoWrapper - will merge these extra options 1134 * @return DDCVideoWrapper 1135 */ 1136 function embed(options) { 1137 var wrapperInstance, 1138 target, 1139 $target, 1140 data, 1141 accountid; 1142 1143 // ************************************ 1144 // Instantiate Wrapper Object
1145 // ************************************ 1146 1147 if (this instanceof DDCVideoWrapperCollection) { 1148 this.embedAll(options); 1149 wrapperInstance = null; 1150 } else if (this instanceof DDCVideoWrapper) { 1151 wrapperInstance = this; 1152 // if we are overriding init options, re-run init 1153 if (options) { 1154 try { 1155 wrapperInstance.init(options); 1156 } catch (e) { 1157 if (window.console && window.console.log) { 1158 console.log('Videoplayer error initializing', e); 1159 } 1160 } 1161 } 1162 } 1163 1164 if (wrapperInstance) { 1165 1166 // ************************************ 1167 // Acquire $target 1168 // ************************************ 1169 1170 $target = acquireTarget(wrapperInstance.options, wrapperInstance); 1171 1172 // sanity check, jwplayer can only have one target 1173 if ($target && $target.length == 1) { 1174 1175 // ************************************ 1176 // Get Any Target Options from Markup 1177 // ************************************ 1178 1179 // nested mergeOptions statement to ensure proper overrides: manual options > data attributes > wrapper defaults 1180 wrapperInstance.options = mergeOptions(wrapperInstance.options, mergeOptions(parseTargetVideoOptions($target), options)); 1181 1182 // ************************************ 1183 // Embed JWPlayer 1184 // ************************************ 1185 1186 // only run embed if playlist is configured and loaded 1187 // init takes care of this asynchronously with the playlist promise objects - make sure they are ready 1188 $.when(jwPlayerScriptPromise, wrapperInstance.playlistPromise).then(function () { 1189 wrapperInstance.onEmbed(); 1190 }); 1191 1192 } else if ($target && $target.length > 1) { 1193 // if there are multiple targets at this point, something is wrong and must run through init again (TODO) 1194 if (window.console && window.console.log) { 1195 console.log('Videoplayer has multiple conflicting video embed target ids'); 1196 } 1197 1198 } else { 1199 if (window.console && window.console.log) { 1200 console.log('Videoplayer could not find embed target element', $target); 1201 } 1202 } 1203 } else { 1204 // no wrapper 1205 } 1206 1207 return wrapperInstance; 1208 } 1209 1210 /** 1211 * Jquery Success function for the successful return of the jwplayer script (which will now provide jwwrapper in global scope) 1212 * 1213 * jwPlayer automatically embeds when it loads - there is no control over this timing in the API 1214 * so we need to ajax load the script every time we want to embed a video (luckily it gets cached) 1215 * 1216 * @param Function fn a user-defined callback function 1217 */ 1218 function onEmbed() { 1219 var wrapperInstance, 1220 jwSettings, 1221 $placeholder, 1222 player, 1223 playerid, 1224 $target, 1225 playlistPath, 1226 called = { 1227 'play' : false, 1228 '0' : false, 1229 '25' : false, 1230 '50' : false, 1231 '75' : false, 1232 '95' : false, 1233 '100' : false 1234 }; 1235 1236 if (this instanceof DDCVideoWrapper && window.jwplayer && this.options.targetid && this.options.playerid) { 1237 wrapperInstance = this; 1238 1239 $target = acquireTarget(wrapperInstance.options, wrapperInstance); 1240 playerid = wrapperInstance.options.playerid; 1241 1242 // If this target already has a jwplayer video in it - remove that cleanly before embedding a new player over it 1243 if (wrapperInstance.jwPlayerInstance && wrapperInstance.jwPlayerInstance.hasOwnProperty('remove')) { 1244 try { 1245 wrapperInstance.jwPlayerInstance.remove(); 1246 } catch (e) { 1247 // "Cannot call remove() before player is ready" 1248 // jwplayer will throw this error if the player is not fully instantiated: 1249 wrapperInstance.playerState = 'initializing'; 1250 } 1251 } 1252 1253 // ************************************ 1254 // INSTANTIATE JWPLAYER 1255 // ************************************ 1256 1257 if (wrapperInstance.playerState != 'initializing') { 1258 1259 wrapperInstance.playerState = 'initializing'; 1260 1261 // insert a temporary placeholder - which will be replaced by the jwplayer 1262 $placeholder = $('<div />').attr('id', playerid).addClass('jwplayer'); 1263 $target.html($placeholder).addClass('jwplayer-loading'); 1264 1265 $placeholder.wrap(wrapperInstance.options.wrapperelement); 1266 1267 jwSettings = getJWSettings(wrapperInstance.options, DDC.Video.jwDefaults); 1268 1269 player = wrapperInstance.jwPlayerInstance = window.jwplayer(playerid).setup(jwSettings); 1270 1271 player.onReady(function () { 1272 wrapperInstance.playerState = 'ready'; 1273 wrapperInstance.playlistPromise = wrapperInstance.prerollPlaylistPromise = wrapperInstance.postrollPlaylistPromise = null; 1274 // jwplayer flash div wrapper doesn't have a .jwplayer class for some reason, so make it consistent with html version for styling 1275 if (this.getRenderingMode() == 'flash') { 1276 $(this.container).parent().addClass('jwplayer jwplayer-flash'); 1277 } else { 1278 $(this.container).addClass('jwplayer jwplayer-html5'); 1279 } 1280 $target.removeClass('jwplayer-loading'); 1281 if (wrapperInstance.onEmbedReadyCallback && typeof wrapperInstance.onEmbedReadyCallback === 'function') { 1282 try { 1283 wrapperInstance.onEmbedReadyCallback.call(player); 1284 } catch (e) { 1285 if (window.console && window.console.log) { 1286 console.log('Videoplayer error in onReady callback', e); 1287 } 1288 } 1289 } 1290 }); 1291 1292 player.onSetupError(function (fallback, message) { 1293 wrapperInstance.playlistPromise = wrapperInstance.prerollPlaylistPromise = wrapperInstance.postrollPlaylistPromise = null; 1294 $('#' + playerid).addClass('jwplayer jwplayer-error'); 1295 if (window.console && window.console.log) { 1296 console.log('Player setup error', fallback, message); 1297 } 1298 }); 1299 1300 // ************************************ 1301 // Item playback fail handler 1302 // ************************************ 1303 1304 player.onError(function (message) { 1305 var myPlaylist, 1306 myIndex; 1307 1308 try { 1309 myPlaylist = this.getPlaylist(); 1310 myIndex = this.getPlaylistIndex(); 1311 1312 if (myPlaylist.length > 1) { 1313 // Remove bad playlist item and reload 1314 myPlaylist.splice(myIndex, 1); 1315 this.load(myPlaylist); 1316 this.play(); 1317 } 1318 1319 } catch (e) { 1320 if (window.console && window.console.log) { 1321 console.log('Player error', message); 1322 } 1323 } 1324 }); 1325 1326 // ************************************ 1327 // Track Events 1328 // ************************************ 1329 1330 playlistPath = getOriginalPath(wrapperInstance.options.ddcplaylist); 1331 1332 player.onPlay(function (e) { 1333 var currentPlaylistItem = this.getPlaylistItem(), 1334 currentPlaylistIndex = this.getPlaylistIndex(), 1335 currentPlayElement = this.getContainer().querySelector('video'), 1336 currentPosition = this.getPosition(), 1337 trackingData = {}; 1338 1339 try { 1340 if (currentPlaylistIndex === 0 && wrapperInstance.hasPreroll && $.cookie) {
1341 $.cookie('prerollViewed', 'true', {expires: 1}); 1342 } 1343 trackingData.eventName = 'play'; 1344 trackingData.playlistPath = playlistPath; 1345 trackingData.videoPath = currentPlaylistItem.file; 1346 1347 DDC.pushToGtmDataLayer({ 1348 event: "ddc.video.play", 1349 eventData: { 1350 element: currentPlayElement, 1351 eventType: "video-play", 1352 videoSource: trackingData.videoPath, 1353 videoDuration: currentPlayElement.duration, 1354 fileType: typeof trackingData.videoPath === 'string' ? trackingData.videoPath.split('.').pop() : '', 1355 position: currentPosition 1356 } 1357 }); 1358 1359 if (!called['play']) { 1360 called['play'] = true; 1361 if (tracker.hasOwnProperty('trackEvent')) { 1362 tracker.trackEvent(trackingData); 1363 } 1364 } 1365 } catch (e) { 1366 if (window.console && window.console.log) { 1367 console.log('tracking error, could not log play'); 1368 } 1369 } 1370 }); 1371 1372 player.onTime(function (e) { 1373 var currentPlaylistItem, 1374 currentPlayElement, 1375 progress = (e.duration > 0) ? e.position / e.duration : 0, 1376 percentPlayed, 1377 trackingData = {}; 1378 1379 try { 1380 1381 if (progress > 0 && !called['0']) { 1382 called['0'] = true; 1383 percentPlayed = '0'; 1384 } else if (progress >= 0.25 && !called['25']) { 1385 called['25'] = true; 1386 percentPlayed = '25'; 1387 } else if (progress >= 0.50 && !called['50']) { 1388 called['50'] = true; 1389 percentPlayed = '50'; 1390 } else if (progress >= 0.75 && !called['75']) { 1391 called['75'] = true; 1392 percentPlayed = '75'; 1393 } else if (progress >= 0.95 && !called['95']) { 1394 called['95'] = true; 1395 percentPlayed = '95'; 1396 } else if (progress >= 0.99 && !called['100']) { 1397 called['100'] = true; 1398 percentPlayed = '100'; 1399 } 1400 1401 if (percentPlayed != undefined) { 1402 currentPlaylistItem = this.getPlaylistItem(); 1403 currentPlayElement = this.getContainer().querySelector('video'); 1404 trackingData.eventName = 'progress'; 1405 trackingData.playlistPath = playlistPath; 1406 trackingData.videoPath = currentPlaylistItem.file; 1407 trackingData.progress = percentPlayed; 1408 if (tracker.hasOwnProperty('trackEvent')) { 1409 tracker.trackEvent(trackingData); 1410 } 1411 1412 if(percentPlayed > 0 && percentPlayed < 100) { 1413 DDC.pushToGtmDataLayer({ 1414 event: "ddc.video.milestone", 1415 eventData: { 1416 element: $target, 1417 eventType: "video-play-milestone", 1418 videoSource: trackingData.videoPath, 1419 videoDuration: e.duration, 1420 videoMilestone: percentPlayed, 1421 fileType: typeof trackingData.videoPath === 'string' ? trackingData.videoPath.split('.').pop() : '' 1422 } 1423 }); 1424 } 1425 } 1426 } catch (e) { 1427 if (window.console && window.console.log) { 1428 console.log('tracking error, could not log onProgress'); 1429 } 1430 } 1431 }); 1432 1433 player.onComplete(function (e) { 1434 var currentPlaylistItem = this.getPlaylistItem(), 1435 currentPlayElement = this.getContainer().querySelector('video'), 1436 trackingData = {}; 1437 1438 try { 1439 trackingData.eventName = 'play'; 1440 trackingData.playlistPath = playlistPath; 1441 trackingData.videoPath = currentPlaylistItem.file; 1442 1443 DDC.pushToGtmDataLayer({ 1444 event: "ddc.video.milestone", 1445 eventData: { 1446 element: currentPlayElement, 1447 eventType: "video-play-milestone", 1448 videoSource: trackingData.videoPath, 1449 videoDuration: currentPlayElement.duration, 1450 videoMilestone: "complete", 1451 fileType: typeof trackingData.videoPath === 'string' ? trackingData.videoPath.split('.').pop() : '' 1452 } 1453 }); 1454 1455 } catch (e) { 1456 if (window.console && window.console.log) { 1457 console.log('tracking error, could not log video complete'); 1458 } 1459 } 1460 }); 1461 1462 } 1463 1464 } else { 1465 if (window.console && window.console.log) { 1466 console.log('Videoplayer could not embed into ' + this.options.targetid); 1467 } 1468 } 1469 } 1470 1471 /** 1472 * A function which is run on each wrapper's onReady callback 1473 * 1474 * @param Function fn A user-defined callback function 1475 */ 1476 function onReady(fn) { 1477 try { 1478 if (typeof fn === 'function' && this instanceof DDCVideoWrapper) { 1479 this.onEmbedReadyCallback = fn; 1480 } 1481 } catch (e) { 1482 if (window.console && window.console.log) { 1483 console.log('Videoplayer could not set up onReady callback', e); 1484 } 1485 } 1486 } 1487
1488 /** 1489 * Set up video specific tracking methods for each type of tracker 1490 * 1491 * @param Object tracker global tracker object 1492 * @treturn Object tracker the configured tracker object, with tracking methods 1493 */ 1494 function configureTracker(tracker, trackerSettings) { 1495 1496 tracker.trackEvent = function (data) { 1497 if (data.hasOwnProperty('eventName')) { 1498 switch (data.eventName) { 1499 1500 case 'play': 1501 if (data.hasOwnProperty('videoPath') && data.hasOwnProperty('playlistPath')) { 1502 if (this.hasOwnProperty('ga')) { 1503 this.gaTrackPlayEvent(data.videoPath, data.playlistPath); 1504 } 1505 } 1506 break; 1507 1508 case 'progress': 1509 if (data.hasOwnProperty('videoPath') && data.hasOwnProperty('playlistPath') && data.hasOwnProperty('progress')) { 1510 if (this.hasOwnProperty('ga')) { 1511 this.gaTrackProgressEvent(data.videoPath, data.playlistPath, data.progress); 1512 } 1513 } 1514 break; 1515 1516 default: 1517 break; 1518 } 1519 } 1520 }; 1521 1522 // ************************************ 1523 // Google Analytics Tracker 1524 // ************************************ 1525 1526 if (tracker.hasOwnProperty('ga')) { 1527 if (DDC.Tracker && DDC.gaManager) { 1528 // DDC.Tracker events automatically tracked by gaManager 1529 delete(tracker.ga); 1530 } else { 1531 // otherwise use this custom GA track code: 1532 configureGATracker(tracker, trackerSettings); 1533 } 1534 } 1535 1536 return tracker; 1537 } 1538 1539 /** 1540 * Set up DDC tracker 1541 * 1542 * @param Object tracker global tracker object 1543 * @treturn Object tracker the configured tracker object, with DDC tracking methods 1544 */ 1545 function configureDDCTracker(tracker, trackerSettings) { 1546 var ddcTrackerCategory = trackerSettings.ddc.viewTypeVideo; 1547 tracker.ddc = DDC.Tracker; 1548 tracker.ddc.deploy(); 1549 1550 tracker.ddcTrackPlayEvent = function (videoPath, playlistPath) { 1551 this.ddcTrackProgressEvent(videoPath, playlistPath, '0'); 1552 }; 1553 1554 tracker.ddcTrackProgressEvent = function (videoPath, playlistPath, progress) { 1555 var viewExtra = videoPath + '|' + playlistPath + '|0|' + progress; 1556 this.ddcTrack(viewExtra); 1557 }; 1558 1559 tracker.ddcTrack = function (viewExtra) { 1560 // DDC.Tracker reference 1561 tracker.ddc.track(ddcTrackerCategory, viewExtra); 1562 }; 1563 1564 return tracker; 1565 } 1566 1567 /** 1568 * Set up google tracker 1569 * 1570 * @param Object tracker 1571 * @treturn Object tracker the configured tracker object, with google tracking methods 1572 */ 1573 function configureGATracker(tracker, settings) { 1574 var ddcGoogleId = settings.ga.id, 1575 gaCategory = settings.ga.category; 1576 1577 tracker.ga = window._gaq = window._gaq || []; 1578 1579 tracker.gaTrackPlayEvent = function (videoPath, playlistPath) { 1580 this.gaTrack(gaCategory, 'play', playlistPath); 1581 }; 1582 1583 tracker.gaTrackProgressEvent = function (videoPath, playlistPath, progress) { 1584 this.gaTrack(gaCategory, 'viewed ' + progress + '%', playlistPath); 1585 }; 1586 1587 tracker.gaTrack = function (category, action, opt_label, opt_value, opt_noninteraction) { 1588 tracker.ga.push(function() { 1589 _gat._getTracker(ddcGoogleId)._trackEvent(category, action, opt_label, opt_value, opt_noninteraction); 1590 }); 1591 }; 1592 } 1593 1594 // ************************************ 1595 // Return Public Module Methods and Props 1596 // ************************************ 1597 return module; 1598 1599})(jQuery);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.