1/** 2 * Basic Ad support plugin for video.js. 3 * 4 * Common code to support ad integrations. 5 */ 6(function(window, document, vjs, undefined) { 7"use strict"; 8 9var 10 11 /** 12 * Copies properties from one or more objects onto an original. 13 */ 14 extend = function(obj /*, arg1, arg2, ... */) { 15 var arg, i, k; 16 for (i=1; i<arguments.length; i++) { 17 arg = arguments[i]; 18 for (k in arg) { 19 if (arg.hasOwnProperty(k)) { 20 obj[k] = arg[k]; 21 } 22 } 23 } 24 return obj; 25 }, 26 27 /** 28 * Add a handler for multiple listeners to an object that supports addEventListener() or on(). 29 * 30 * @param {object} obj The object to which the handler will be assigned. 31 * @param {mixed} events A string, array of strings, or hash of string/callback pairs. 32 * @param {function} callback Invoked when specified events occur, if events param is not a hash. 33 * 34 * @return {object} obj The object passed in. 35 */ 36 on = function(obj, events, handler) { 37 38 var 39 40 type = Object.prototype.toString.call(events), 41 42 register = function(obj, event, handler) { 43 if (obj.addEventListener) { 44 obj.addEventListener(event, handler); 45 } else if (obj.on) { 46 obj.on(event, handler); 47 } else if (obj.attachEvent) { 48 obj.attachEvent('on' + event, handler); 49 } else { 50 throw new Error('object has no mechanism for adding event listeners'); 51 } 52 }, 53 54 i, 55 ii; 56 57 switch (type) { 58 case '[object String]': 59 register(obj, events, handler); 60 break; 61 case '[object Array]': 62 for (i = 0, ii = events.length; i<ii; i++) { 63 register(obj, events[i], handler); 64 } 65 break; 66 case '[object Object]': 67 for (i in events) { 68 if (events.hasOwnProperty(i)) { 69 register(obj, i, events[i]); 70 } 71 } 72 break; 73 default: 74 throw new Error('Unrecognized events parameter type: ' + type); 75 } 76 77 return obj; 78 79 }, 80 81 /** 82 * Runs the callback at the next available opportunity. 83 * @see https://developer.mozilla.org/en-US/docs/Web/API/window.setImmediate 84 */ 85 setImmediate = function(callback) { 86 return ( 87 window.setImmediate || 88 window.requestAnimationFrame || 89 window.mozRequestAnimationFrame || 90 window.webkitRequestAnimationFrame || 91 window.setTimeout 92 )(callback, 0); 93 }, 94 95 /** 96 * Clears a callback previously registered with `setImmediate`. 97 * @param {id} id The identifier of the callback to abort 98 */ 99 clearImmediate = function(id) { 100 return (window.clearImmediate || 101 window.cancelAnimationFrame || 102 window.webkitCancelAnimationFrame || 103 window.mozCancelAnimationFrame || 104 window.clearTimeout)(id); 105 }, 106 107 /** 108 * If ads are not playing, pauses the player at the next available 109 * opportunity. Has no effect if ads have started. This function is necessary 110 * because pausing a video element while processing a `play` event on iOS can 111 * cause the video element to continuously toggle between playing and paused 112 * states. 113 * 114 * @param {object} player The video player 115 */ 116 cancelContentPlay = function(player) { 117 if (player.ads.cancelPlayTimeout) { 118 // another cancellation is already in flight, so do nothing 119 return; 120 } 121 122 player.ads.cancelPlayTimeout = setImmediate(function() { 123 124 // deregister the cancel timeout so subsequent cancels are scheduled 125 player.ads.cancelPlayTimeout = null; 126 127 if (!player.paused()) { 128 player.pause(); 129 } 130 }); 131 }, 132 133 /** 134 * Returns an object that captures the portions of player state relevant to 135 * video playback. The result of this function can be passed to 136 * restorePlayerSnapshot with a player to return the player to the state it 137 * was in when this function was invoked. 138 * @param {object} player The videojs player object 139 */ 140 getPlayerSnapshot = function(player) { 141 var 142 tech = player.el().querySelector('.vjs-tech'), 143 snapshot = { 144 src: player.currentSrc(), 145 currentTime: player.currentTime(), 146 type: player.currentType() 147 }; 148 149 if (tech) { 150 snapshot.nativePoster = tech.poster; 151 } 152 153 return snapshot; 154 }, 155 156 removeClass = function(element, className) { 157 var 158 classes = element.className.split(/\s+/), 159 i = classes.length, 160 newClasses = []; 161 while (i--) { 162 if (classes[i] !== className) { 163 newClasses.push(classes[i]); 164 } 165 } 166 element.className = newClasses.join(' '); 167 }, 168 169 /** 170 * Attempts to modify the specified player so that its state is equivalent to 171 * the state of the snapshot. 172 * @param {object} snapshot - the player state to apply 173 */ 174 restorePlayerSnapshot = function(player, snapshot) { 175 var 176 // the playback tech
177 tech = player.el().querySelector('.vjs-tech'), 178 179 // the number of remaining attempts to restore the snapshot 180 attempts = 20, 181 182 // finish restoring the playback state 183 resume = function() { 184 player.currentTime(snapshot.currentTime); 185 //If this wasn't a postroll resume 186 if (!player.ended()) { 187 player.play(); 188 } 189 }, 190 191 // determine if the video element has loaded enough of the snapshot source 192 // to be ready to apply the rest of the state 193 tryToResume = function() { 194 if (tech.seekable === undefined) { 195 // if the tech doesn't expose the seekable time ranges, try to 196 // resume playback immediately 197 resume(); 198 return; 199 } 200 if (tech.seekable.length > 0) { 201 // if some period of the video is seekable, resume playback 202 resume(); 203 return; 204 } 205 206 // delay a bit and then check again unless we're out of attempts 207 if (attempts--) { 208 setTimeout(tryToResume, 50); 209 } 210 }, 211 212 // whether the video element has been modified since the 213 // snapshot was taken 214 srcChanged; 215 216 if (snapshot.nativePoster) { 217 tech.poster = snapshot.nativePoster; 218 } 219 220 // Determine whether the player needs to be restored to its state 221 // before ad playback began. With a custom ad display or burned-in 222 // ads, the content player state hasn't been modified and so no 223 // restoration is required 224 225 if (player.src()) { 226 // the player was in src attribute mode before the ad and the 227 // src attribute has not been modified, no restoration is required 228 // to resume playback 229 srcChanged = player.src() !== snapshot.src; 230 } else { 231 // the player was configured through source element children 232 // and the currentSrc hasn't changed, no restoration is required 233 // to resume playback 234 srcChanged = player.currentSrc() !== snapshot.src; 235 } 236 237 if (srcChanged) { 238 // if the src changed for ad playback, reset it 239 player.src({ src: snapshot.src, type: snapshot.type }); 240 // safari requires a call to `load` to pick up a changed source 241 player.load(); 242 // and then resume from the snapshots time once the original src has loaded 243 player.one('loadedmetadata', tryToResume); 244 } else if (!player.ended()) { 245 // the src didn't change and this wasn't a postroll 246 // just resume playback at the current time. 247 player.play(); 248 } 249 }, 250 251 /** 252 * Remove the poster attribute from the video element tech, if present. When 253 * reusing a video element for multiple videos, the poster image will briefly 254 * reappear while the new source loads. Removing the attribute ahead of time 255 * prevents the poster from showing up between videos. 256 * @param {object} player The videojs player object 257 */ 258 removeNativePoster = function(player) { 259 var tech = player.el().querySelector('.vjs-tech'); 260 if (tech) { 261 tech.removeAttribute('poster'); 262 } 263 }, 264 265 // --------------------------------------------------------------------------- 266 // Ad Framework 267 // --------------------------------------------------------------------------- 268 269 // default framework settings 270 defaults = { 271 // maximum amount of time in ms to wait to receive `adsready` from the ad 272 // implementation after play has been requested. Ad implementations are 273 // expected to load any dynamic libraries and make any requests to determine 274 // ad policies for a video during this time. 275 timeout: 5000, 276 277 // maximum amount of time in ms to wait for the ad implementation to start 278 // linear ad mode after `readyforpreroll` has fired. This is in addition to 279 // the standard timeout. 280 prerollTimeout: 100, 281 282 // when truthy, instructs the plugin to output additional information about 283 // plugin state to the video.js log. On most devices, the video.js log is 284 // the same as the developer console. 285 debug: false 286 }, 287 288 adFramework = function(options) { 289 var 290 player = this, 291 292 // merge options and defaults 293 settings = extend({}, defaults, options || {}), 294 295 fsmHandler; 296 297 // replace the ad initializer with the ad namespace 298 player.ads = { 299 state: 'content-set', 300 301 startLinearAdMode: function() { 302 player.trigger('adstart'); 303 }, 304 305 endLinearAdMode: function() { 306 player.trigger('adend'); 307 } 308 }; 309 310 fsmHandler = function(event) { 311 312 // Ad Playback State Machine 313 var 314 fsm = { 315 'content-set': { 316 events: { 317 'adsready': function() { 318 this.state = 'ads-ready'; 319 }, 320 'play': function() { 321 this.state = 'ads-ready?'; 322 cancelContentPlay(player); 323 324 // remove the poster so it doesn't flash between videos 325 removeNativePoster(player); 326 } 327 } 328 }, 329 'ads-ready': { 330 events: { 331 'play': function() { 332 this.state = 'preroll?'; 333 cancelContentPlay(player); 334 } 335 } 336 }, 337 'preroll?': { 338 enter: function() { 339 // change class to show that we're waiting on ads
340 player.el().className += ' vjs-ad-loading'; 341 342 // schedule an adtimeout event to fire if we waited too long 343 player.ads.timeout = window.setTimeout(function() { 344 player.trigger('adtimeout'); 345 }, settings.prerollTimeout); 346 347 // signal to ad plugin that it's their opportunity to play a preroll 348 player.trigger('readyforpreroll'); 349 350 }, 351 leave: function() { 352 window.clearTimeout(player.ads.timeout); 353 354 clearImmediate(player.ads.cancelPlayTimeout); 355 player.ads.cancelPlayTimeout = null; 356 357 removeClass(player.el(), 'vjs-ad-loading'); 358 }, 359 events: { 360 'play': function() { 361 cancelContentPlay(player); 362 }, 363 'adstart': function() { 364 this.state = 'ad-playback'; 365 player.el().className += ' vjs-ad-playing'; 366 }, 367 'adtimeout': function() { 368 this.state = 'content-playback'; 369 player.play(); 370 } 371 } 372 }, 373 'ads-ready?': { 374 enter: function() { 375 player.el().className += ' vjs-ad-loading'; 376 player.ads.timeout = window.setTimeout(function() { 377 player.trigger('adtimeout'); 378 }, settings.timeout); 379 }, 380 leave: function() { 381 window.clearTimeout(player.ads.timeout); 382 removeClass(player.el(), 'vjs-ad-loading'); 383 }, 384 events: { 385 'play': function() { 386 cancelContentPlay(player); 387 }, 388 'adsready': function() { 389 this.state = 'preroll?'; 390 }, 391 'adtimeout': function() { 392 this.state = 'ad-timeout-playback'; 393 } 394 } 395 }, 396 'ad-timeout-playback': { 397 events: { 398 'adsready': function() { 399 if (player.paused()) { 400 this.state = 'ads-ready'; 401 } else { 402 this.state = 'preroll?'; 403 } 404 }, 405 'contentupdate': function() { 406 if (player.paused()) { 407 this.state = 'content-set'; 408 } else { 409 this.state = 'ads-ready?'; 410 } 411 } 412 } 413 }, 414 'ad-playback': { 415 enter: function() { 416 // capture current player state snapshot (playing, currentTime, src) 417 this.snapshot = getPlayerSnapshot(player); 418 419 // remove the poster so it doesn't flash between videos 420 removeNativePoster(player); 421 }, 422 leave: function() { 423 removeClass(player.el(), 'vjs-ad-playing'); 424 restorePlayerSnapshot(player, this.snapshot); 425 }, 426 events: { 427 'adend': function() { 428 this.state = 'content-playback'; 429 } 430 } 431 }, 432 'content-playback': { 433 events: { 434 'adstart': function() { 435 this.state = 'ad-playback'; 436 player.el().className += ' vjs-ad-playing'; 437 438 // remove the poster so it doesn't flash between videos 439 removeNativePoster(player); 440 }, 441 'contentupdate': function() { 442 if (player.paused()) { 443 this.state = 'content-set'; 444 } else { 445 this.state = 'ads-ready?'; 446 } 447 } 448 } 449 } 450 }; 451 452 (function(state) { 453 454 var noop = function() {}; 455 456 // process the current event with a noop default handler 457 (fsm[state].events[event.type] || noop).apply(player.ads); 458 459 // execute leave/enter callbacks if present 460 if (state !== player.ads.state) { 461 (fsm[state].leave || noop).apply(player.ads);
462 (fsm[player.ads.state].enter || noop).apply(player.ads); 463 464 if (settings.debug) { 465 videojs.log('ads', state + ' -> ' + player.ads.state); 466 } 467 } 468 469 })(player.ads.state); 470 471 }; 472 473 // register for the events we're interested in 474 on(player, vjs.Html5.Events.concat([ 475 // events emitted by ad plugin 476 'adtimeout', 477 'contentupdate', 478 // events emitted by third party ad implementors 479 'adsready', 480 'adstart', // startLinearAdMode() 481 'adend' // endLinearAdMode() 482 ]), fsmHandler); 483 484 // keep track of the current content source 485 // if you want to change the src of the video without triggering 486 // the ad workflow to restart, you can update this variable before 487 // modifying the player's source 488 player.ads.contentSrc = player.currentSrc(); 489 490 // implement 'contentupdate' event. 491 (function(){ 492 var 493 // check if a new src has been set, if so, trigger contentupdate 494 checkSrc = function() { 495 var src; 496 if (player.ads.state !== 'ad-playback') { 497 src = player.currentSrc(); 498 if (src !== player.ads.contentSrc) { 499 player.trigger({ 500 type: 'contentupdate', 501 oldValue: player.ads.contentSrc, 502 newValue: src 503 }); 504 player.ads.contentSrc = src; 505 } 506 } 507 }; 508 // loadstart reliably indicates a new src has been set 509 player.on('loadstart', checkSrc); 510 // check immediately in case we missed the loadstart 511 setImmediate(checkSrc); 512 })(); 513 514 // kick off the fsm 515 if (!player.paused()) { 516 // simulate a play event if we're autoplaying 517 fsmHandler({type:'play'}); 518 } 519 520 }; 521 522 // register the ad plugin framework 523 vjs.plugin('ads', adFramework); 524 525})(window, document, videojs);
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.