1// ------------------------------------------------------------------------------------------- 2// 3// AVIA VIDEO API - make sure that youtube, vimeo and html 5 use the same interface 4// 5// requires froogaloop vimeo library and youtube iframe api (yt can be loaded async) 6// 7// ------------------------------------------------------------------------------------------- 8 9 10(function($) 11{ 12 "use strict"; 13 14 $.AviaVideoAPI = function(options, video, option_container) 15 { 16 this.videoElement = video; 17 18 // actual video element. either iframe or video 19 this.$video = $( video ); 20 21 // container where the AviaVideoAPI object will be stored as data, and that can receive events like play, pause etc 22 // also the container that allows to overwrite javacript options by adding html data- attributes 23 this.$option_container = option_container ? $( option_container ) : this.$video; 24 25 // button to click to actually start the loading process of the video 26 this.load_btn = this.$option_container.find('.av-click-to-play-overlay'); 27 28 // contains video list 29 this.video_wrapper = this.$video.parents('ul').eq(0); 30 31 //check if we should load immediately or on click 32 this.lazy_load = this.video_wrapper.hasClass('av-show-video-on-click') ? true : false; 33 34 //mobile device? 35 this.isMobile = $.avia_utilities.isMobile; 36 37 //iamge fallback use 38 this.fallback = this.isMobile ? this.$option_container.is('.av-mobile-fallback-image') : false; 39 40 if( this.fallback ) 41 { 42 return; 43 } 44 45 // set up the whole api object 46 this._init( options ); 47 }; 48 49 $.AviaVideoAPI.defaults = { 50 51 loop: false, 52 mute: false, 53 controls: false, 54 events: 'play pause mute unmute loop toggle reset unload' 55 56 }; 57 58 $.AviaVideoAPI.apiFiles = 59 { 60 youtube : {loaded: false, src: 'https://www.youtube.com/iframe_api' } 61 }; 62 63 $.AviaVideoAPI.players = 64 { 65 66 }; 67 68 $.AviaVideoAPI.prototype = 69 { 70 _init: function( options ) 71 { 72 // set slider options 73 this.options = this._setOptions(options); 74 75 // info which video service we are using: html5, vimeo or youtube 76 this.type = this._getPlayerType(); 77 78 this.player = false; 79 80 // store the player object to the this.player variable created by one of the APIs (mediaelement, youtube, vimeo) 81 this._bind_player(); 82 83 // set to true once the events are bound so it doesnt happen a second time by accident or racing condition 84 this.eventsBound = false; 85 86 // info if the video is playing 87 this.playing = false; 88 89 //set css class that video is currently not playing 90 this.$option_container.addClass('av-video-paused'); 91 92 //play pause indicator 93 this.pp = $.avia_utilities.playpause(this.$option_container); 94 }, 95 96 97 //set the video options by first merging the default options and the passed options, then checking the video element if any data attributes overwrite the option set 98 _setOptions: function(options) 99 { 100 var newOptions = $.extend( true, {}, $.AviaVideoAPI.defaults, options ), 101 htmlData = this.$option_container.data(), 102 i = ""; 103 104 //overwritte passed option set with any data properties on the html element 105 for( i in htmlData ) 106 { 107 if( htmlData.hasOwnProperty(i) && (typeof htmlData[i] === "string" || typeof htmlData[i] === "number" || typeof htmlData[i] === "boolean") ) 108 { 109 newOptions[i] = htmlData[i]; 110 } 111 } 112 113 return newOptions; 114 }, 115 116 //get the player type 117 _getPlayerType: function() 118 { 119 var vid_src = this.$video.get(0).src || this.$video.data('src'); 120 121 if( this.$video.is('video') ) 122 { 123 return 'html5'; 124 } 125 126 if( this.$video.is('.av_youtube_frame') ) 127 { 128 return 'youtube'; 129 } 130 131 if( vid_src.indexOf('vimeo.com') != -1 ) 132 { 133 return 'vimeo'; 134 } 135 if( vid_src.indexOf('youtube.com') != -1) 136 { 137 return 'youtube'; 138 } 139 }, 140 141 _bind_player: function() 142 { 143 var _self = this; 144 145 // check if videos are disabled by user setting via cookie - or user must opt in. 146 var cookie_check = $('html').hasClass('av-cookies-needs-opt-in') || $('html').hasClass('av-cookies-can-opt-out'); 147 var allow_continue = true;
148 var silent_accept_cookie = $('html').hasClass('av-cookies-user-silent-accept'); 149 var self_hosted = 'html5' == this.type; 150 151 if( cookie_check && ! silent_accept_cookie && ! self_hosted ) 152 { 153 if( ! document.cookie.match(/aviaCookieConsent/) || $('html').hasClass('av-cookies-session-refused') ) 154 { 155 allow_continue = false; 156 } 157 else 158 { 159 if( ! document.cookie.match(/aviaPrivacyRefuseCookiesHideBar/) ) 160 { 161 allow_continue = false; 162 } 163 else if( ! document.cookie.match(/aviaPrivacyEssentialCookiesEnabled/) ) 164 { 165 allow_continue = false; 166 } 167 else if( document.cookie.match(/aviaPrivacyVideoEmbedsDisabled/) ) 168 { 169 allow_continue = false; 170 } 171 } 172 } 173 174 // allow 3-rd party plugins to hook (see enfold\config-cookiebot\cookiebot.js) 175 if( window['wp'] && wp.hooks ) 176 { 177 allow_continue = wp.hooks.applyFilters( 'aviaCookieConsent_allow_continue', allow_continue ); 178 } 179 180 let allow_class = allow_continue ? 'avia-video-cookie-allowed' : 'avia-video-cookie-not-allowed'; 181 $('html').addClass( allow_class ); 182 183 if( ! allow_continue ) 184 { 185 this._use_external_link(); 186 return; 187 } 188 189 if(this.lazy_load && this.load_btn.length && this.type != "html5") 190 { 191 this.$option_container.addClass('av-video-lazyload'); 192 193 this.load_btn.on('click', function() 194 { 195 _self.load_btn.remove(); 196 _self._setPlayer(); 197 }); 198 } 199 else 200 { 201 this.lazy_load = false; 202 this._setPlayer(); 203 } 204 }, 205 206 //if the user has disabled video slides via cookie a click event will open the video externally 207 _use_external_link: function() 208 { 209 //display the play button no matter what 210 this.$option_container.addClass('av-video-lazyload'); 211 212 this.load_btn.on('click', function(e) 213 { 214 if (e.originalEvent === undefined) return; //human click only 215 216 var src_url = $(this).parents('.avia-slide-wrap').find('div[data-original_url]').data('original_url');
217 if( src_url ) window.open(src_url , '_blank'); 218 }); 219 }, 220 221 //get the player object 222 _setPlayer: function() 223 { 224 var _self = this; 225 226 switch( this.type ) 227 { 228 case "html5": 229 230 this.player = this.$video.data('mediaelementplayer'); 231 232 //apply fallback. sometimes needed for safari 233 if( ! this.player ) 234 { 235 this.$video.data('mediaelementplayer', $.AviaVideoAPI.players[ this.$video.attr('id').replace(/_html5/,'') ] ); 236 this.player = this.$video.data('mediaelementplayer'); 237 } 238 239 this._playerReady(); 240 break; 241 242 case "vimeo": 243 244 //we need to create an iframe and append it to the html first 245 var ifrm = document.createElement("iframe"); 246 var $ifrm = $( ifrm ); 247 248 //note: unmuted vimeo videos in chrome do often times not work due to chrome blocking them 249 ifrm.onload = function() 250 { 251 _self.player = Froogaloop( ifrm ); 252 _self._playerReady(); 253 _self.$option_container.trigger('av-video-loaded'); 254 }; 255 256 ifrm.setAttribute("src", this.$video.data('src') ); 257 258 //we replace the old html structure with the iframe 259 $ifrm.insertAfter( this.$video ); 260 this.$video.remove(); 261 this.$video = ifrm; 262 break; 263 264 case "youtube": 265 266 this._getAPI(this.type); 267 $('body').on('av-youtube-iframe-api-loaded', function(){ _self._playerReady(); }); 268 break; 269 } 270 }, 271 272 _getAPI: function( api ) 273 { 274 //make sure the api file is loaded only once 275 if($.AviaVideoAPI.apiFiles[api].loaded === false) 276 { 277 $.AviaVideoAPI.apiFiles[api].loaded = true; 278 //load the file async 279 var tag = document.createElement('script'), 280 first = document.getElementsByTagName('script')[0]; 281 282 tag.src = $.AviaVideoAPI.apiFiles[api].src; 283 first.parentNode.insertBefore(tag, first); 284 } 285 }, 286 287 //wait for player to be ready, then bind events 288 _playerReady: function() 289 { 290 var _self = this; 291 292 this.$option_container.on('av-video-loaded', function(){ _self._bindEvents(); }); 293 294 switch(this.type) 295 { 296 case "html5": 297 this.$video.on( 'av-mediajs-loaded', function(){ _self.$option_container.trigger('av-video-loaded'); } ); 298 this.$video.on( 'av-mediajs-ended' , function(){ _self.$option_container.trigger('av-video-ended'); } ); 299 break; 300 case "vimeo": 301 //finish event must be applied after ready event for firefox 302 _self.player.addEvent('ready', function() 303 { 304 _self.$option_container.trigger('av-video-loaded'); 305 _self.player.addEvent('finish', function(){ _self.$option_container.trigger('av-video-ended'); }); 306 }); 307 break; 308 309 case "youtube": 310 311 var params = _self.$video.data(); 312 313 if( _self._supports_video() ) 314 { 315 params.html5 = 1; 316 } 317 318 var youtube_config = { 319 videoId: params.videoid, 320 height: _self.$video.attr('height'), 321 width: _self.$video.attr('width'), 322 playerVars: params 323 }; 324 325 /** 326 * "Use Privacy-Enhanced Video URLs" theme option - youtube-nocookie.com avoids 327 * setting cookies until the visitor actually interacts with the video. 328 * 329 * @since 8.2 330 */ 331 if( typeof avia_framework_globals !== 'undefined' && avia_framework_globals.privacyEnhancedVideo === true ) 332 { 333 youtube_config.host = 'https://www.youtube-nocookie.com'; 334 } 335 336 youtube_config.events = { 337 'onReady': function(){ _self.$option_container.trigger('av-video-loaded'); }, 338 'onError': function(player){ $.avia_utilities.log('YOUTUBE ERROR:', 'error', player); }, 339 'onStateChange': function(event){ 340 if (event.data === YT.PlayerState.ENDED) 341 { 342 var command = _self.options.loop != false ? 'loop' : 'av-video-ended'; 343 _self.$option_container.trigger(command); 344 } 345 } 346 }; 347 348 _self.player = new YT.Player(_self.$video.attr('id'), youtube_config); 349 break; 350 } 351 352 //fallback always trigger after 2 seconds 353 setTimeout(function() 354 { 355 if(_self.eventsBound == true || typeof _self.eventsBound == 'undefined' || _self.type == 'youtube' ) 356 { 357 return; 358 } 359 360 $.avia_utilities.log('Fallback Video Trigger "' + _self.type + '":', 'log', _self ); 361 362 _self.$option_container.trigger('av-video-loaded'); 363 364 }, 2000 ); 365 }, 366 367 //bind events we should listen to, to the player 368 _bindEvents: function() 369 { 370 if( this.eventsBound == true || typeof this.eventsBound == 'undefined') 371 { 372 return; 373 } 374 375 var _self = this, volume = 'unmute'; 376 377 this.eventsBound = true; 378 379 this.$option_container.on(this.options.events, function(e) 380 { 381 _self.api(e.type); 382 }); 383 384 if( !_self.isMobile ) 385 { 386 //set up initial options 387 if( this.options.mute != false ) 388 { 389 volume = "mute"; 390 } 391 392 if( this.options.loop != false ) 393 { 394 _self.api('loop'); 395 } 396 397 _self.api(volume); 398 } 399 400 //set timeout to prevent racing conditions with other scripts 401 setTimeout( function() 402 {
403 _self.$option_container.trigger('av-video-events-bound').addClass('av-video-events-bound'); 404 }, 50 ); 405 }, 406 407 408 _supports_video: function() 409 { 410 return !!document.createElement('video').canPlayType; 411 }, 412 413 414 /************************************************************************ 415 PUBLIC Methods 416 *************************************************************************/ 417 418 419 api: function( action ) 420 { 421 //commands on mobile can not be executed if the player was not started manually 422 if(this.isMobile && !this.was_started()) return; 423 424 // prevent calling of unbound function 425 if(this.options.events.indexOf(action) === -1) return; 426 427 // broadcast that the command was executed 428 this.$option_container.trigger('av-video-'+action+'-executed'); 429 430 // console.log("video player api action: " + action); 431 432 // calls the function based on action. eg: _html5_play() 433 if(typeof this[ '_' + this.type + '_' + action] == 'function') 434 { 435 this[ '_' + this.type + '_' + action].call(this); 436 } 437 438 //call generic function eg: _toggle() or _play() 439 if(typeof this[ '_' + action] == 'function') 440 { 441 this[ '_' + action].call(this); 442 } 443 }, 444 445 was_started: function() 446 { 447 if(!this.player) return false; 448 449 switch(this.type) 450 { 451 case "html5": 452 if( this.player.getCurrentTime() > 0 ) 453 { 454 return true; 455 } 456 break; 457 458 case "vimeo": 459 if( this.player.api('getCurrentTime') > 0 ) 460 { 461 return true; 462 } 463 break; 464 465 case "youtube": 466 if( this.player.getPlayerState() !== -1 ) 467 { 468 return true; 469 } 470 break; 471 } 472 473 return false; 474 }, 475 476 /************************************************************************ 477 Generic Methods, are always executed and usually set variables 478 *************************************************************************/ 479 _play: function() 480 { 481 this.playing = true; 482 this.$option_container.addClass('av-video-playing').removeClass('av-video-paused'); 483 }, 484 485 _pause: function() 486 { 487 this.playing = false; 488 this.$option_container.removeClass('av-video-playing').addClass('av-video-paused'); 489 }, 490 491 _loop: function() 492 { 493 this.options.loop = true; 494 }, 495 496 _toggle: function( ) 497 { 498 var command = this.playing == true ? 'pause' : 'play'; 499 500 this.api(command); 501 this.pp.set(command); 502 }, 503 504 505 /************************************************************************ 506 VIMEO Methods 507 *************************************************************************/ 508 509 _vimeo_play: function( ) 510 { 511 this.player.api('play'); 512 }, 513 514 _vimeo_pause: function( ) 515 { 516 this.player.api('pause'); 517 }, 518 519 _vimeo_mute: function( ) 520 { 521 this.player.api('setVolume', 0); 522 }, 523 524 _vimeo_unmute: function( ) 525 { 526 this.player.api('setVolume', 0.7); 527 }, 528 529 _vimeo_loop: function( ) 530 { 531 // currently throws error, must be set in iframe 532 // this.player.api('setLoop', true); 533 }, 534 535 _vimeo_reset: function( ) 536 { 537 this.player.api('seekTo',0); 538 }, 539 540 _vimeo_unload: function() 541 { 542 this.player.api('unload'); 543 }, 544 545 /************************************************************************ 546 YOUTUBE Methods 547 *************************************************************************/ 548 549 _youtube_play: function( ) 550 { 551 this.player.playVideo(); 552 }, 553 554 _youtube_pause: function( ) 555 { 556 this.player.pauseVideo(); 557 }, 558 559 _youtube_mute: function( ) 560 { 561 this.player.mute(); 562 }, 563 564 _youtube_unmute: function( ) 565 { 566 this.player.unMute(); 567 }, 568 569 _youtube_loop: function( ) 570 { 571 // does not work properly with iframe api. needs to manual loop on "end" event 572 // this.player.setLoop(true); 573 if(this.playing == true) this.player.seekTo(0); 574 }, 575 576 _youtube_reset: function( ) 577 { 578 this.player.stopVideo(); 579 }, 580 581 _youtube_unload: function() 582 { 583 this.player.clearVideo(); 584 }, 585 586 /************************************************************************ 587 HTML5 Methods 588 *************************************************************************/ 589 590 _html5_play: function( ) 591 { 592 //disable stoping of other videos in case the user wants to run section bgs 593 if(this.player) 594 { 595 this.player.options.pauseOtherPlayers = false;
596 this.player.play(); 597 } 598 599 }, 600 601 _html5_pause: function( ) 602 { 603 if(this.player) this.player.pause(); 604 }, 605 606 _html5_mute: function( ) 607 { 608 if(this.player) this.player.setMuted(true); 609 }, 610 611 _html5_unmute: function( ) 612 { 613 if(this.player) this.player.setVolume(0.7); 614 }, 615 616 _html5_loop: function( ) 617 { 618 if(this.player) this.player.options.loop = true; 619 }, 620 621 _html5_reset: function( ) 622 { 623 if(this.player) this.player.setCurrentTime(0); 624 }, 625 626 _html5_unload: function() 627 { 628 this._html5_pause(); 629 this._html5_reset(); 630 } 631 }; 632 633 //simple wrapper to call the api. makes sure that the api data is not applied twice 634 $.fn.aviaVideoApi = function( options , apply_to_parent) 635 { 636 return this.each( function() 637 { 638 // by default save the object as data to the initial video. 639 // in the case of slideshows its more benefitial to save it to a parent element (eg: the slide) 640 var applyTo = this; 641 642 if( apply_to_parent ) 643 { 644 applyTo = $(this).parents(apply_to_parent).get(0); 645 } 646 647 var self = $.data( applyTo, 'aviaVideoApi' ); 648 649 if( ! self ) 650 { 651 self = $.data( applyTo, 'aviaVideoApi', new $.AviaVideoAPI( options, this, applyTo ) ); 652 } 653 }); 654 }; 655 656})( jQuery ); 657 658window.onYouTubeIframeAPIReady = function(){ jQuery('body').trigger('av-youtube-iframe-api-loaded'); }; 659 660 661// Init style shamelessly stolen from jQuery http://jquery.com 662// min version: https://f.vimeocdn.com/js/froogaloop2.min.js 663var Froogaloop = (function(){ 664 // Define a local copy of Froogaloop 665 function Froogaloop(iframe) { 666 // The Froogaloop object is actually just the init constructor 667 return new Froogaloop.fn.init(iframe); 668 } 669 670 var eventCallbacks = {}, 671 hasWindowEvent = false, 672 isReady = false, 673 slice = Array.prototype.slice, 674 playerOrigin = '*'; 675 676 Froogaloop.fn = Froogaloop.prototype = { 677 element: null, 678 679 init: function(iframe) { 680 if (typeof iframe === "string") { 681 iframe = document.getElementById(iframe); 682 } 683 684 this.element = iframe; 685 686 return this; 687 }, 688 689 /* 690 * Calls a function to act upon the player. 691 * 692 * @param {string} method The name of the Javascript API method to call. Eg: 'play'. 693 * @param {Array|Function} valueOrCallback params Array of parameters to pass when calling an API method 694 * or callback function when the method returns a value. 695 */ 696 api: function(method, valueOrCallback) { 697 if (!this.element || !method) { 698 return false; 699 } 700 701 var self = this, 702 element = self.element, 703 target_id = element.id !== '' ? element.id : null, 704 params = !isFunction(valueOrCallback) ? valueOrCallback : null, 705 callback = isFunction(valueOrCallback) ? valueOrCallback : null; 706 707 // Store the callback for get functions 708 if (callback) { 709 storeCallback(method, callback, target_id); 710 } 711 712 postMessage(method, params, element); 713 return self; 714 }, 715 716 /* 717 * Registers an event listener and a callback function that gets called when the event fires. 718 * 719 * @param eventName (String): Name of the event to listen for. 720 * @param callback (Function): Function that should be called when the event fires. 721 */ 722 addEvent: function(eventName, callback) { 723 if (!this.element) { 724 return false; 725 } 726 727 var self = this, 728 element = self.element, 729 target_id = element.id !== '' ? element.id : null; 730 731 732 storeCallback(eventName, callback, target_id); 733 734 // The ready event is not registered via postMessage. It fires regardless. 735 if (eventName != 'ready') { 736 postMessage('addEventListener', eventName, element); 737 } 738 else if (eventName == 'ready' && isReady) { 739 callback.call(null, target_id); 740 } 741 742 return self; 743 }, 744 745 /* 746 * Unregisters an event listener that gets called when the event fires. 747 * 748 * @param eventName (String): Name of the event to stop listening for. 749 */ 750 removeEvent: function(eventName) { 751 if (!this.element) { 752 return false;
753 } 754 755 var self = this, 756 element = self.element, 757 target_id = element.id !== '' ? element.id : null, 758 removed = removeCallback(eventName, target_id); 759 760 // The ready event is not registered 761 if (eventName != 'ready' && removed) { 762 postMessage('removeEventListener', eventName, element); 763 } 764 } 765 }; 766 767 /** 768 * Handles posting a message to the parent window. 769 * 770 * @param method (String): name of the method to call inside the player. For api calls 771 * this is the name of the api method (api_play or api_pause) while for events this method 772 * is api_addEventListener. 773 * @param params (Object or Array): List of parameters to submit to the method. Can be either 774 * a single param or an array list of parameters. 775 * @param target (HTMLElement): Target iframe to post the message to. 776 */ 777 function postMessage(method, params, target) { 778 if (!target.contentWindow.postMessage) { 779 return false; 780 } 781 782 var data = JSON.stringify({ 783 method: method, 784 value: params 785 }); 786 787 target.contentWindow.postMessage(data, playerOrigin); 788 } 789 790 /** 791 * Event that fires whenever the window receives a message from its parent 792 * via window.postMessage. 793 */ 794 function onMessageReceived(event) 795 { 796 797 var data, method; 798 799 try { 800 data = JSON.parse(event.data); 801 method = data.event || data.method; 802 } 803 catch(e) { 804 //fail silently... like a ninja! 805 } 806 807 if (method == 'ready' && !isReady) { 808 isReady = true; 809 } 810 811 // Handles messages from the vimeo player only 812 if (!(/^https?:\/\/player.vimeo.com/).test(event.origin)) { 813 return false; 814 } 815 816 if (playerOrigin === '*') { 817 playerOrigin = event.origin; 818 } 819 var value = data.value, 820 eventData = data.data, 821 target_id = target_id === '' ? null : data.player_id, 822 823 callback = getCallback(method, target_id), 824 params = []; 825 826 if (!callback) { 827 return false; 828 } 829 830 if (value !== undefined) { 831 params.push(value); 832 } 833 834 if (eventData) { 835 params.push(eventData); 836 } 837 838 if (target_id) { 839 params.push(target_id); 840 } 841 842 return params.length > 0 ? callback.apply(null, params) : callback.call(); 843 } 844 845 846 /** 847 * Stores submitted callbacks for each iframe being tracked and each 848 * event for that iframe. 849 * 850 * @param eventName (String): Name of the event. Eg. api_onPlay 851 * @param callback (Function): Function that should get executed when the 852 * event is fired. 853 * @param target_id (String) [Optional]: If handling more than one iframe then 854 * it stores the different callbacks for different iframes based on the iframe's 855 * id. 856 */ 857 function storeCallback(eventName, callback, target_id) { 858 if (target_id) { 859 if (!eventCallbacks[target_id]) { 860 eventCallbacks[target_id] = {}; 861 } 862 eventCallbacks[target_id][eventName] = callback; 863 } 864 else { 865 eventCallbacks[eventName] = callback; 866 } 867 } 868 869 /** 870 * Retrieves stored callbacks. 871 */ 872 function getCallback(eventName, target_id) { 873 874 /*modified by kriesi - removing this will result in a js error. */ 875 if (target_id && eventCallbacks[target_id] && eventCallbacks[target_id][eventName]) 876 { 877 return eventCallbacks[target_id][eventName]; 878 } 879 else 880 { 881 return eventCallbacks[eventName]; 882 } 883 } 884 885 function removeCallback(eventName, target_id) { 886 if (target_id && eventCallbacks[target_id]) { 887 if (!eventCallbacks[target_id][eventName]) { 888 return false; 889 } 890 eventCallbacks[target_id][eventName] = null; 891 } 892 else { 893 if (!eventCallbacks[eventName]) { 894 return false;
895 } 896 eventCallbacks[eventName] = null; 897 } 898 899 return true; 900 } 901 902 function isFunction(obj) { 903 return !!(obj && obj.constructor && obj.call && obj.apply); 904 } 905 906 function isArray(obj) { 907 return toString.call(obj) === '[object Array]'; 908 } 909 910 // Give the init function the Froogaloop prototype for later instantiation 911 Froogaloop.fn.init.prototype = Froogaloop.fn; 912 913 // Listens for the message event. 914 // W3C 915 if (window.addEventListener) { 916 window.addEventListener('message', onMessageReceived, false); 917 } 918 // IE 919 else { 920 window.attachEvent('onmessage', onMessageReceived); 921 } 922 923 // Expose froogaloop to the global object 924 return (window.Froogaloop = window.$f = Froogaloop); 925 926})();
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.