1(function(factory){ 2 "use strict"; 3 if (typeof define === 'function' && define.amd) { 4 define('videojs-contrib-ads', ['videojs'], function(vjs){factory(window, document, vjs);}); 5 } else if (typeof exports === 'object' && typeof module === 'object') { 6 factory(window, document, require('video.js')); 7 } else { 8 factory(window, document, videojs); 9 } 10}) 11/** 12 * Basic Ad support plugin for video.js. 13 * 14 * Common code to support ad integrations. 15 */ 16(function(window, document, vjs, undefined) { 17"use strict"; 18 19var 20 21 /** 22 * Copies properties from one or more objects onto an original. 23 */ 24 extend = function(obj /*, arg1, arg2, ... */) { 25 var arg, i, k; 26 for (i=1; i<arguments.length; i++) { 27 arg = arguments[i]; 28 for (k in arg) { 29 if (arg.hasOwnProperty(k)) { 30 obj[k] = arg[k]; 31 } 32 } 33 } 34 return obj; 35 }, 36 37 /** 38 * Add a handler for multiple listeners to an object that supports addEventListener() or on(). 39 * 40 * @param {object} obj The object to which the handler will be assigned. 41 * @param {mixed} events A string, array of strings, or hash of string/callback pairs. 42 * @param {function} callback Invoked when specified events occur, if events param is not a hash. 43 * 44 * @return {object} obj The object passed in. 45 */ 46 on = function(obj, events, handler) { 47 48 var 49 50 type = Object.prototype.toString.call(events), 51 52 register = function(obj, event, handler) { 53 if (obj.addEventListener) { 54 obj.addEventListener(event, handler); 55 } else if (obj.on) { 56 obj.on(event, handler); 57 } else if (obj.attachEvent) { 58 obj.attachEvent('on' + event, handler); 59 } else { 60 throw new Error('object has no mechanism for adding event listeners'); 61 } 62 }, 63 64 i, 65 ii; 66 67 switch (type) { 68 case '[object String]': 69 register(obj, events, handler); 70 break; 71 case '[object Array]': 72 for (i = 0, ii = events.length; i<ii; i++) { 73 register(obj, events[i], handler); 74 } 75 break; 76 case '[object Object]': 77 for (i in events) { 78 if (events.hasOwnProperty(i)) { 79 register(obj, i, events[i]); 80 } 81 } 82 break; 83 default: 84 throw new Error('Unrecognized events parameter type: ' + type); 85 } 86 87 return obj; 88 89 }, 90 91 /** 92 * Runs the callback at the next available opportunity. 93 * @see https://developer.mozilla.org/en-US/docs/Web/API/window.setImmediate 94 */ 95 setImmediate = function(callback) { 96 return ( 97 window.setImmediate || 98 window.requestAnimationFrame || 99 window.mozRequestAnimationFrame || 100 window.webkitRequestAnimationFrame || 101 window.setTimeout 102 )(callback, 0); 103 }, 104 105 /** 106 * Clears a callback previously registered with `setImmediate`. 107 * @param {id} id The identifier of the callback to abort 108 */ 109 clearImmediate = function(id) { 110 return (window.clearImmediate || 111 window.cancelAnimationFrame || 112 window.webkitCancelAnimationFrame || 113 window.mozCancelAnimationFrame || 114 window.clearTimeout)(id); 115 }, 116 117 /** 118 * If ads are not playing, pauses the player at the next available 119 * opportunity. Has no effect if ads have started. This function is necessary 120 * because pausing a video element while processing a `play` event on iOS can 121 * cause the video element to continuously toggle between playing and paused 122 * states. 123 * 124 * @param {object} player The video player 125 */ 126 cancelContentPlay = function(player) { 127 if (player.ads.cancelPlayTimeout) { 128 // another cancellation is already in flight, so do nothing 129 return; 130 } 131 player.ads.cancelPlayTimeout = setImmediate(function() { 132 // deregister the cancel timeout so subsequent cancels are scheduled 133 player.ads.cancelPlayTimeout = null; 134 135 // pause playback so ads can be handled. 136 if (!player.paused()) { 137 player.pause(); 138 } 139 140 // add a contentplayback handler to resume playback when ads finish. 141 player.one('contentplayback', function() { 142 if (player.paused()) { 143 player.play(); 144 } 145 }); 146 }); 147 }, 148 149 /** 150 * Returns an object that captures the portions of player state relevant to 151 * video playback. The result of this function can be passed to 152 * restorePlayerSnapshot with a player to return the player to the state it 153 * was in when this function was invoked. 154 * @param {object} player The videojs player object 155 */ 156 getPlayerSnapshot = function(player) { 157 var
158 tech = player.el().querySelector('.vjs-tech'), 159 tracks = player.remoteTextTracks ? player.remoteTextTracks() : [], 160 track, 161 i, 162 suppressedTracks = [], 163 snapshot = { 164 ended: player.ended(), 165 src: player.currentSrc(), 166 currentTime: player.currentTime(), 167 type: player.currentType() 168 }; 169 170 if (tech) { 171 snapshot.nativePoster = tech.poster; 172 snapshot.style = tech.getAttribute('style'); 173 } 174 175 i = tracks.length; 176 while (i--) { 177 track = tracks[i]; 178 suppressedTracks.push({ 179 track: track, 180 mode: track.mode 181 }); 182 track.mode = 'disabled'; 183 } 184 snapshot.suppressedTracks = suppressedTracks; 185 186 return snapshot; 187 }, 188 189 removeClass = function(element, className) { 190 var 191 classes = element.className.split(/\s+/), 192 i = classes.length, 193 newClasses = []; 194 while (i--) { 195 if (classes[i] !== className) { 196 newClasses.push(classes[i]); 197 } 198 } 199 element.className = newClasses.join(' '); 200 }, 201 202 /** 203 * Attempts to modify the specified player so that its state is equivalent to 204 * the state of the snapshot. 205 * @param {object} snapshot - the player state to apply 206 */ 207 restorePlayerSnapshot = function(player, snapshot) { 208 var 209 // the playback tech 210 tech = player.el().querySelector('.vjs-tech'), 211 212 // the number of remaining attempts to restore the snapshot 213 attempts = 20, 214 215 suppressedTracks = snapshot.suppressedTracks, 216 trackSnapshot, 217 restoreTracks = function() { 218 var i = suppressedTracks.length; 219 while (i--) { 220 trackSnapshot = suppressedTracks[i]; 221 trackSnapshot.track.mode = trackSnapshot.mode; 222 } 223 }, 224 225 // finish restoring the playback state 226 resume = function() { 227 var 228 ended = false, 229 updateEnded = function() { 230 ended = true; 231 }; 232 player.currentTime(snapshot.currentTime); 233 234 // Resume playback if this wasn't a postroll 235 if (!snapshot.ended) { 236 player.play(); 237 } else { 238 // On iOS 8.1, the "ended" event will not fire if you seek 239 // directly to the end of a video. To make that behavior 240 // consistent with the standard, fire a synthetic event if 241 // "ended" does not fire within 250ms. Note that the ended 242 // event should occur whether the browser actually has data 243 // available for that position 244 // (https://html.spec.whatwg.org/multipage/embedded-content.html#seeking), 245 // so it should not be necessary to wait for the seek to 246 // indicate completion. 247 window.setTimeout(function() { 248 if (!ended) { 249 player.play(); 250 } 251 player.off('ended', updateEnded); 252 }, 250); 253 player.on('ended', updateEnded); 254 } 255 }, 256 257 // determine if the video element has loaded enough of the snapshot source 258 // to be ready to apply the rest of the state 259 tryToResume = function() { 260 if (tech.readyState > 1) { 261 // some browsers and media aren't "seekable". 262 // readyState greater than 1 allows for seeking without exceptions 263 return resume(); 264 } 265 266 if (tech.seekable === undefined) { 267 // if the tech doesn't expose the seekable time ranges, try to 268 // resume playback immediately 269 return resume(); 270 } 271 272 if (tech.seekable.length > 0) { 273 // if some period of the video is seekable, resume playback 274 return resume(); 275 } 276 277 // delay a bit and then check again unless we're out of attempts 278 if (attempts--) { 279 setTimeout(tryToResume, 50); 280 } else { 281 (function() { 282 try { 283 resume(); 284 } catch(e) { 285 videojs.log.warn('Failed to resume the content after an advertisement', e); 286 } 287 })(); 288 } 289 }, 290 291 // whether the video element has been modified since the 292 // snapshot was taken 293 srcChanged; 294 295 if (snapshot.nativePoster) { 296 tech.poster = snapshot.nativePoster; 297 } 298 299 if ('style' in snapshot) { 300 // overwrite all css style properties to restore state precisely 301 tech.setAttribute('style', snapshot.style || ''); 302 } 303 304 // Determine whether the player needs to be restored to its state 305 // before ad playback began. With a custom ad display or burned-in 306 // ads, the content player state hasn't been modified and so no 307 // restoration is required 308 309 if (player.src()) { 310 // the player was in src attribute mode before the ad and the 311 // src attribute has not been modified, no restoration is required 312 // to resume playback 313 srcChanged = player.src() !== snapshot.src; 314 } else { 315 // the player was configured through source element children 316 // and the currentSrc hasn't changed, no restoration is required 317 // to resume playback 318 srcChanged = player.currentSrc() !== snapshot.src; 319 } 320 321 if (srcChanged) { 322 // on ios7, fiddling with textTracks too early will cause safari to crash 323 player.one('contentloadedmetadata', restoreTracks); 324 325 // if the src changed for ad playback, reset it 326 player.src({ src: snapshot.src, type: snapshot.type }); 327 // safari requires a call to `load` to pick up a changed source 328 player.load(); 329 // and then resume from the snapshots time once the original src has loaded 330 player.one('contentcanplay', tryToResume); 331 } else if (!player.ended() || !snapshot.ended) { 332 // if we didn't change the src, just restore the tracks 333 restoreTracks(); 334 335 // the src didn't change and this wasn't a postroll 336 // just resume playback at the current time. 337 player.play(); 338 } 339 }, 340 341 /**
342 * Remove the poster attribute from the video element tech, if present. When 343 * reusing a video element for multiple videos, the poster image will briefly 344 * reappear while the new source loads. Removing the attribute ahead of time 345 * prevents the poster from showing up between videos. 346 * @param {object} player The videojs player object 347 */ 348 removeNativePoster = function(player) { 349 var tech = player.el().querySelector('.vjs-tech'); 350 if (tech) { 351 tech.removeAttribute('poster'); 352 } 353 }, 354 355 // --------------------------------------------------------------------------- 356 // Ad Framework 357 // --------------------------------------------------------------------------- 358 359 // default framework settings 360 defaults = { 361 // maximum amount of time in ms to wait to receive `adsready` from the ad 362 // implementation after play has been requested. Ad implementations are 363 // expected to load any dynamic libraries and make any requests to determine 364 // ad policies for a video during this time. 365 timeout: 5000, 366 367 // maximum amount of time in ms to wait for the ad implementation to start 368 // linear ad mode after `readyforpreroll` has fired. This is in addition to 369 // the standard timeout. 370 prerollTimeout: 100, 371 372 // maximum amount of time in ms to wait for the ad implementation to start 373 // linear ad mode after `contentended` has fired. 374 postrollTimeout: 100, 375 376 // when truthy, instructs the plugin to output additional information about 377 // plugin state to the video.js log. On most devices, the video.js log is 378 // the same as the developer console. 379 debug: false 380 }, 381 382 adFramework = function(options) { 383 var 384 player = this, 385 386 // merge options and defaults 387 settings = extend({}, defaults, options || {}), 388 389 fsmHandler; 390 391 // prefix all video element events during ad playback 392 // if the video element emits ad-related events directly, 393 // plugins that aren't ad-aware will break. prefixing allows 394 // plugins that wish to handle ad events to do so while 395 // avoiding the complexity for common usage 396 (function() { 397 var 398 videoEvents = videojs.Html5.Events, 399 i = videoEvents.length, 400 returnTrue = function() { return true; }, 401 triggerEvent = function(type, event) { 402 // pretend we called stopImmediatePropagation because we want the native 403 // element events to continue propagating 404 event.isImmediatePropagationStopped = returnTrue; 405 event.cancelBubble = true; 406 event.isPropagationStopped = returnTrue; 407 player.trigger({ 408 type: type + event.type, 409 state: player.ads.state, 410 originalEvent: event 411 }); 412 }, 413 redispatch = function(event) { 414 if (player.ads.state === 'ad-playback') { 415 triggerEvent('ad', event); 416 417 } else if (player.ads.state === 'content-playback' && event.type === 'ended') { 418 triggerEvent('content', event); 419 420 } else if (player.ads.state === 'content-resuming') { 421 if (player.ads.snapshot) { 422 // the video element was recycled for ad playback 423 if (player.currentSrc() !== player.ads.snapshot.src) { 424 if (event.type === 'loadstart') { 425 return; 426 } 427 return triggerEvent('content', event); 428 429 // we ended playing postrolls and the video itself 430 // the content src is back in place 431 } else if (player.ads.snapshot.ended) { 432 if ((event.type === 'pause' || 433 event.type === 'ended')) { 434 // after loading a video, the natural state is to not be started 435 // in this case, it actually has, so, we do it manually 436 player.addClass('vjs-has-started'); 437 // let `pause` and `ended` events through, naturally 438 return; 439 } 440 // prefix all other events in content-resuming with `content` 441 return triggerEvent('content', event); 442 } 443 } 444 if (event.type !== 'playing') { 445 triggerEvent('content', event); 446 } 447 } 448 }; 449 450 while (i--) { 451 player.on(videoEvents[i], redispatch); 452 } 453 return redispatch; 454 })(); 455 456 // We now auto-play when an ad gets loaded if we're playing ads in the same video element as the content. 457 // The problem is that in IE11, we cannot play in addurationchange but in iOS8, we cannot play from adcanplay. 458 // This will allow ad-integrations from needing to do this themselves. 459 player.on(['addurationchange', 'adcanplay'], function() { 460 var snapshot = player.ads.snapshot; 461 if (player.currentSrc() === snapshot.src) { 462 return; // do nothing 463 } 464 465 player.play(); 466 }); 467 468 // replace the ad initializer with the ad namespace 469 player.ads = { 470 state: 'content-set', 471 472 startLinearAdMode: function() { 473 player.trigger('adstart'); 474 }, 475 476 endLinearAdMode: function() { 477 player.trigger('adend'); 478 } 479 }; 480 481 fsmHandler = function(event) { 482 // Ad Playback State Machine 483 var 484 fsm = { 485 'content-set': { 486 events: { 487 'adscanceled': function() { 488 this.state = 'content-playback'; 489 }, 490 'adsready': function() { 491 this.state = 'ads-ready'; 492 }, 493 'play': function() { 494 this.state = 'ads-ready?'; 495 cancelContentPlay(player); 496 // remove the poster so it doesn't flash between videos 497 removeNativePoster(player); 498 }, 499 'adserror': function() { 500 this.state = 'content-playback'; 501 } 502 } 503 }, 504 'ads-ready': { 505 events: { 506 'play': function() { 507 this.state = 'preroll?'; 508 cancelContentPlay(player); 509 }, 510 'adserror': function() { 511 this.state = 'content-playback'; 512 } 513 } 514 }, 515 'preroll?': { 516 enter: function() { 517 // change class to show that we're waiting on ads
518 player.el().className += ' vjs-ad-loading'; 519 // schedule an adtimeout event to fire if we waited too long 520 player.ads.timeout = window.setTimeout(function() { 521 player.trigger('adtimeout'); 522 }, settings.prerollTimeout); 523 // signal to ad plugin that it's their opportunity to play a preroll 524 player.trigger('readyforpreroll'); 525 }, 526 leave: function() { 527 window.clearTimeout(player.ads.timeout); 528 removeClass(player.el(), 'vjs-ad-loading'); 529 }, 530 events: { 531 'play': function() { 532 cancelContentPlay(player); 533 }, 534 'adstart': function() { 535 this.state = 'ad-playback'; 536 player.el().className += ' vjs-ad-playing'; 537 }, 538 'adtimeout': function() { 539 this.state = 'content-playback'; 540 }, 541 'adserror': function() { 542 this.state = 'content-playback'; 543 } 544 } 545 }, 546 'ads-ready?': { 547 enter: function() { 548 player.el().className += ' vjs-ad-loading'; 549 player.ads.timeout = window.setTimeout(function() { 550 player.trigger('adtimeout'); 551 }, settings.timeout); 552 }, 553 leave: function() { 554 window.clearTimeout(player.ads.timeout); 555 removeClass(player.el(), 'vjs-ad-loading'); 556 }, 557 events: { 558 'play': function() { 559 cancelContentPlay(player); 560 }, 561 'adscanceled': function() { 562 this.state = 'content-playback'; 563 }, 564 'adsready': function() { 565 this.state = 'preroll?'; 566 }, 567 'adtimeout': function() { 568 this.state = 'content-playback'; 569 }, 570 'adserror': function() { 571 this.state = 'content-playback'; 572 } 573 } 574 }, 575 'ad-playback': { 576 enter: function() { 577 // capture current player state snapshot (playing, currentTime, src) 578 this.snapshot = getPlayerSnapshot(player); 579 580 // remove the poster so it doesn't flash between videos 581 removeNativePoster(player); 582 // We no longer need to supress play events once an ad is playing. 583 // Clear it if we were. 584 if (player.ads.cancelPlayTimeout) { 585 clearImmediate(player.ads.cancelPlayTimeout); 586 player.ads.cancelPlayTimeout = null; 587 } 588 }, 589 leave: function() { 590 removeClass(player.el(), 'vjs-ad-playing'); 591 592 restorePlayerSnapshot(player, this.snapshot); 593 if (player.ads.triggerevent !== 'adend') { 594 // trigger 'adend' as a consistent notification 595 // event that we're exiting ad-playback. 596 player.trigger('adend'); 597 } 598 }, 599 events: { 600 'adend': function() { 601 this.state = 'content-resuming'; 602 }, 603 'adserror': function() { 604 this.state = 'content-resuming'; 605 } 606 } 607 }, 608 'content-resuming': { 609 enter: function() { 610 if (this.snapshot.ended) { 611 window.clearTimeout(player.ads._fireEndedTimeout); 612 // in some cases, ads are played in a swf or another video element 613 // so we do not get an ended event in this state automatically. 614 // If we don't get an ended event we can use, we need to trigger 615 // one ourselves or else we won't actually ever end the current video. 616 player.ads._fireEndedTimeout = window.setTimeout(function() { 617 player.trigger('ended'); 618 }, 1000); 619 } 620 }, 621 leave: function() { 622 window.clearTimeout(player.ads._fireEndedTimeout); 623 }, 624 events: { 625 'contentupdate': function() { 626 this.state = 'content-set'; 627 }, 628 'playing': function() { 629 this.state = 'content-playback'; 630 }, 631 'ended': function() { 632 this.state = 'content-playback'; 633 } 634 } 635 }, 636 'postroll?': { 637 enter: function() { 638 this.snapshot = getPlayerSnapshot(player); 639
640 player.el().className += ' vjs-ad-loading'; 641 642 player.ads.timeout = window.setTimeout(function() { 643 player.trigger('adtimeout'); 644 }, settings.postrollTimeout); 645 }, 646 leave: function() { 647 window.clearTimeout(player.ads.timeout); 648 removeClass(player.el(), 'vjs-ad-loading'); 649 }, 650 events: { 651 'adstart': function() { 652 this.state = 'ad-playback'; 653 player.el().className += ' vjs-ad-playing'; 654 }, 655 'adtimeout': function() { 656 this.state = 'content-resuming'; 657 setImmediate(function() { 658 player.trigger('ended'); 659 }); 660 }, 661 'adserror': function() { 662 this.state = 'content-resuming'; 663 setImmediate(function() { 664 player.trigger('ended'); 665 }); 666 } 667 } 668 }, 669 'content-playback': { 670 enter: function() { 671 // make sure that any cancelPlayTimeout is cleared 672 if (player.ads.cancelPlayTimeout) { 673 clearImmediate(player.ads.cancelPlayTimeout); 674 player.ads.cancelPlayTimeout = null; 675 } 676 // this will cause content to start if a user initiated 677 // 'play' event was canceled earlier. 678 player.trigger({ 679 type: 'contentplayback', 680 triggerevent: player.ads.triggerevent 681 }); 682 }, 683 events: { 684 // in the case of a timeout, adsready might come in late. 685 'adsready': function() { 686 player.trigger('readyforpreroll'); 687 }, 688 'adstart': function() { 689 this.state = 'ad-playback'; 690 player.el().className += ' vjs-ad-playing'; 691 // remove the poster so it doesn't flash between videos 692 removeNativePoster(player); 693 }, 694 'contentupdate': function() { 695 if (player.paused()) { 696 this.state = 'content-set'; 697 } else { 698 this.state = 'ads-ready?'; 699 } 700 }, 701 'contentended': function() { 702 this.state = 'postroll?'; 703 } 704 } 705 } 706 }; 707 708 (function(state) { 709 var noop = function() {}; 710 711 // process the current event with a noop default handler 712 ((fsm[state].events || {})[event.type] || noop).apply(player.ads); 713 714 // check whether the state has changed 715 if (state !== player.ads.state) { 716 717 // record the event that caused the state transition 718 player.ads.triggerevent = event.type; 719 720 // execute leave/enter callbacks if present 721 (fsm[state].leave || noop).apply(player.ads); 722 (fsm[player.ads.state].enter || noop).apply(player.ads); 723 724 // output debug logging 725 if (settings.debug) { 726 videojs.log('ads', player.ads.triggerevent + ' triggered: ' + state + ' -> ' + player.ads.state); 727 } 728 } 729 730 })(player.ads.state); 731 732 }; 733 734 // register for the events we're interested in 735 on(player, vjs.Html5.Events.concat([ 736 // events emitted by ad plugin 737 'adtimeout', 738 'contentupdate', 739 'contentplaying', 740 'contentended', 741 742 // events emitted by third party ad implementors 743 'adsready', 744 'adserror', 745 'adscanceled', 746 'adstart', // startLinearAdMode() 747 'adend' // endLinearAdMode() 748 ]), fsmHandler); 749 750 // keep track of the current content source 751 // if you want to change the src of the video without triggering 752 // the ad workflow to restart, you can update this variable before 753 // modifying the player's source 754 player.ads.contentSrc = player.currentSrc(); 755 756 // implement 'contentupdate' event. 757 (function(){ 758 var 759 // check if a new src has been set, if so, trigger contentupdate 760 checkSrc = function() { 761 var src; 762 if (player.ads.state !== 'ad-playback') { 763 src = player.currentSrc(); 764 if (src !== player.ads.contentSrc) { 765 player.trigger({ 766 type: 'contentupdate', 767 oldValue: player.ads.contentSrc, 768 newValue: src 769 }); 770 player.ads.contentSrc = src; 771 } 772 } 773 }; 774 // loadstart reliably indicates a new src has been set 775 player.on(['loadstart'], checkSrc); 776 // check immediately in case we missed the loadstart 777 setImmediate(checkSrc); 778 })(); 779 780 // kick off the fsm 781 if (!player.paused()) { 782 // simulate a play event if we're autoplaying 783 fsmHandler({type:'play'}); 784 } 785 786 }; 787 788 // register the ad plugin framework 789 vjs.plugin('ads', adFramework); 790 791});
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.