├── .gitattributes ├── .github └── dependabot.yml ├── .gitignore ├── LICENSE ├── README.md ├── index.html ├── jquery.youtube-background.js ├── jquery.youtube-background.js.map ├── jquery.youtube-background.min.js ├── package-lock.json ├── package.json ├── poops.json ├── script ├── build ├── publish └── server ├── src ├── experimental.js ├── lib │ ├── buttons.js │ ├── controls.js │ ├── super-video-background.js │ ├── video-background.js │ ├── vimeo-background.js │ └── youtube-background.js ├── main.js └── video-backgrounds.js ├── style.css ├── youtube-background-experimental.js ├── youtube-background-experimental.js.map └── youtube-background-experimental.min.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for npm 4 | - package-ecosystem: "npm" 5 | directory: "/" 6 | schedule: 7 | interval: "monthly" 8 | allow: 9 | - dependency-type: "production" 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node artifact files 2 | node_modules/ 3 | package-lock.json 4 | 5 | # Generated by MacOS 6 | .DS_Store 7 | 8 | # Generated by Windows 9 | Thumbs.db 10 | 11 | .vscode 12 | .history 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Nikola Stamatovic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 📺 youtube-background 2 | [![npm version](https://img.shields.io/npm/v/youtube-background)](https://www.npmjs.com/package/youtube-background) 3 | [![CSS gzip size](https://img.badgesize.io/stamat/youtube-background/master/jquery.youtube-background.min.js?compression=gzip&label=gzip%20size)](https://github.com/stamat/youtube-background/blob/master/jquery.youtube-background.js) 4 | 5 | > Create video backgrounds from a YouTube, Vimeo or video file links. 6 | 7 | **⚠️ Future development will be moved to [stamat/video-backgrounds](https://github.com/stamat/video-backgrounds).** Support will still be provided for this repo. 8 | 9 | [DEMO HERE ➡️](http://stamat.github.io/youtube-background/) 10 | 11 | This project started as a simple 100 liner jQuery plugin for YouTube video backgrounds. The idea behind it was to have a straightforward minimal way to add a YouTube video as a background for a div, or any other HTML element. It was intended to be used on hero and banner elements mostly. You would add a data attribute `data-vbg` to the element, and the script would take care of the rest, no CSS required. 12 | 13 | **Vanilla** 14 | 15 | ```html 16 |
17 | 18 | 21 | ``` 22 | **jQuery** 23 | ```html 24 |
25 | 26 | 33 | ``` 34 | 35 | Since it's creation it has evolved to support Vimeo and video files as well. Numerous features were added out of necessity on other projects or by community requests. 36 | 37 | After numerous iterations and is now a fully fledged ES module that can be used with or without jQuery. It is also available as a standalone script. 38 | 39 | ## Features 40 | 41 | * **No CSS required** - the script takes care of everything 42 | * **YouTube**, **Vimeo** and **video files** support 43 | * **jQuery** plugin and **ESM** module 44 | * **Lazyloading** - lazyload the iframe/video 45 | * YouTube and Vimeo **cookies** are disabled by default 46 | * YouTube and Vimeo player API scrips are loaded only when needed 47 | 48 | ## Installation 49 | 50 | ### As a ESM module 51 | 52 | To install the package from NPM run: 53 | ``` 54 | npm install youtube-background 55 | ``` 56 | 57 | Then import the script just like any other ESM module (if your bundler supports resolving `node_modules`, your import will look like this, otherwise you'll have to provide the full path to the script): 58 | 59 | ``` 60 | import 'youtube-background'; 61 | ``` 62 | 63 | If you are using a bundler and you wish to use this script as a jQuery plugin, don't forget to import jQuery too. 64 | 65 | ### Over CDN 66 | 67 | ``` 68 | 69 | ``` 70 | or minified: 71 | ``` 72 | 73 | ``` 74 | 75 | ## Usage 76 | 77 | There are two ways to use this script: vanilla implementation or as a jQuery plugin. 78 | 79 | ### Vanilla Way 80 | **As of version 1.0.6 jQuery is no longer a dependency**, but purely optional. To initialize video backgrounds without jQuery use the global class: `new VideoBackgrounds('[data-vbg]');`. 81 | 82 | ```html 83 |
84 | ``` 85 | 86 | ```javascript 87 | import VideoBackgrounds from 'youtube-background'; // or if you are loading it from CDN as a standalone script, you can use the global variable `VideoBackgrounds` 88 | 89 | const videoBackgrounds = new VideoBackgrounds('[data-vbg]'); 90 | ``` 91 | 92 | `VideoBackgrounds` is a factory class - this means that it is used to create and index multiple instances of the video backgrounds depending on the link type: YouTube, Vimeo or video file. It accepts a selector as a parameter and properties object that will be applied to all of the instances queried by the selector. For the list of available properties, please refer to the [Properties](#properties) section. 93 | 94 | In order to programmatically add a new element to the factory instance and initialize the video background, for instance on an async event. You can use the `add` function of the factory instance, which accepts the element object and the optional properties object. For the list of available properties, please refer to the [Properties](#properties) section. 95 | 96 | ```javascript 97 | // get the first element 98 | const firstElement = document.querySelector('[data-vbg]'); 99 | 100 | // add the element to the factory instance 101 | videoBackgrounds.add(firstElement); 102 | ``` 103 | 104 | In order to automatically initialize video backgrounds on all elements that match the selector as they appear in the DOM, you will have to implement MutationObserver manually. 105 | 106 | The factory instance also indexes all of the individual video background instances by generated UID in it's property `index`, so you can access them later on if you need to. 107 | 108 | UID is assigned to all target elements as a `data-vbg-uid` attribute when the video background is initialized. You can get the instance of the element by using a `get` function of the factory instance, which accepts the UID string or element object with UID attribute. 109 | 110 | ```javascript 111 | // get the first element 112 | const firstElement = document.querySelector('[data-vbg]'); 113 | 114 | // get the first instance instance by UID 115 | const firstInstance = videoBackgrounds.get(firstElement); 116 | ``` 117 | 118 | You can programmatically control the video playing in the background regardless of the provider and access all of it's properties via the instance object. 119 | 120 | ```javascript 121 | // true if the video is playing, false if the video is not playing 122 | console.log(firstInstance.playing); 123 | 124 | // true if video is muted, false if video is not muted 125 | console.log(firstInstance.muted); 126 | 127 | // true if the video is intersecting the viewport, false if the video is not intersecting the viewport. 128 | console.log(firstInstance.isIntersecting); 129 | 130 | // current state of the video 131 | console.log(firstInstance.currentState); 132 | 133 | // current time of the video in seconds 134 | console.log(firstInstance.currentTime); 135 | 136 | // percentage of the video that has been played 137 | console.log(firstInstance.percentComplete); 138 | 139 | // the element that the video background is attached to. `firstElement` from the above example 140 | console.log(firstInstance.element); 141 | 142 | // the element of the video player, meaning either an iframe in case of YouTube and Vimeo, or a video element 143 | console.log(firstInstance.playerElement); 144 | 145 | // the video player object, meaning either a YouTube or Vimeo player object, or a video element in case of HTML5 video 146 | console.log(firstInstance.player); 147 | 148 | // the type of the video, can be `youtube`, `vimeo` or `video` 149 | console.log(firstInstance.type) 150 | 151 | // volume of the video from 0 to 1 152 | console.log(firstInstance.volume); 153 | 154 | // play the video 155 | firstInstance.play(); 156 | 157 | // pause the video 158 | firstInstance.pause(); 159 | 160 | // mute the video 161 | firstInstance.mute(); 162 | 163 | // unmute the video 164 | firstInstance.unmute(); 165 | 166 | // set the video source 167 | firstInstance.setSource('https://www.youtube.com/watch?v=eEpEeyqGlxA'); 168 | 169 | // set the video volume 170 | firstInstance.setVolume(0.5); 171 | 172 | // volume of the video from 0 to 1, or in case of Vimeo a promise that resolves to the volume value 173 | firstInstance.getVolume(); 174 | 175 | // seek the video to a specific percentage complete 176 | firstInstance.seek(25); 177 | 178 | // seek the video to a specific time in seconds 179 | firstInstance.seekTo(1.25); 180 | 181 | // set Start At seconds 182 | firstInstance.setStartAt(10); 183 | 184 | // set End At in seconds 185 | firstInstance.setEndAt(20); 186 | ``` 187 | 188 | If you wish to tune to the videos events, you can add listeners to the element that you've initialized the video background on. In `event.detail` you will get the instance object of the video background. Do refer to the [Events](#events) section for the list of all events. 189 | 190 | ```javascript 191 | firstElement.addEventListener('video-background-ready', function(event) { 192 | console.log('video-background-ready'); // the video instance object 193 | console.log(event.detail); // the video instance object 194 | }) 195 | ``` 196 | 197 | In order to destroy the video background instance and revert the element to it's pre-initialization state, you can use the `destroy` function of the factory instance. 198 | 199 | ```javascript 200 | // destroy the video background by providing the element 201 | videoBackgrounds.destroy(firstElement); 202 | 203 | // or by providing the instance videoBackground.destroy(firstInstance); 204 | ``` 205 | 206 | To destroy all the instances in the index you can use `destroyAll` function of the factory instance. 207 | 208 | Factory instance also implements the `IntersectionObserver` out of the box to keep track of the visible video backgrounds in order to toggle their play/pause state and preserve the bandwidth and improve the performance. You can find the instance of the `IntersectionObserver` in the `intersectionObserver` property of the factory instance. 209 | 210 | For the resize events, the factory instance implements the `ResizeObserver` out of the box. You can find the instance of the `ResizeObserver` in the `resizeObserver` property of the factory instance. If the `resizeObserver` is not supported, the factory instance will fallback to the `window` resize event. 211 | 212 | ### jQuery Way 213 | 214 | jQuery is no longer a dependency, but purely optional. To initialize video backgrounds with jQuery use the global function: `jQuery('[data-vbg]').youtube_background();`. 215 | 216 | ```html 217 |
218 | ``` 219 | 220 | ```javascript 221 | jQuery(document).ready(function() { 222 | jQuery('[data-vbg]').youtube_background(); 223 | }); 224 | ``` 225 | 226 | This function does exactly the same thing as if you would initialize the ES6 factory class. It will pass the selected elements and initialize the factory class in the global variable `VIDEO_BACKGROUNDS`. So everything that applies for the factory instance in the ES6 guide applies to this instance. 227 | 228 | ```javascript 229 | // get the first element 230 | const firstElement = $('[data-vbg]')[0]; 231 | 232 | // get the first instance instance by UID 233 | const firstInstance = VIDEO_BACKGROUNDS.get(firstElement); 234 | ``` 235 | 236 | The plugin method accepts properties object as a parameter. For the list of available properties, please refer to the next [Properties](#properties) section. 237 | 238 | ```javascript 239 | jQuery(document).ready(function() { 240 | jQuery('[data-vbg]').youtube_background({ 241 | 'play-button': true 242 | }); 243 | }); 244 | ``` 245 | 246 | ## Properties 247 | 248 | Property | Default | Accepts | Description 249 | -------- | ------- | ------- | ----------- 250 | **play-button** | false | boolean | Adds a toggle pause button 251 | **mute-button** | false | boolean | Adds a toggle mute button 252 | **autoplay** | true | boolean | Autoplay loaded video 253 | **muted** | true | boolean | Load video muted 254 | **loop** | true | boolean | Loop loaded video 255 | **mobile** | false | boolean | Keep the youtube embed on mobile 256 | **fit-box** | false | boolean | Set iframe to fit the container, meaning `width: 100%; height: 100%` 257 | **inline-styles** | true | boolean | Enable/disable inline styles from the iframe and wrapper. The default wrapper styles are: `background-size: cover;`, `background-repeat: no-repeat;` and `background-position: center;`; the default iframe styles are `top: 50%;`, `left: 50%;`, `transform: translateX(-50%) translateY(-50%);`, `position: absolute;`, and `opacity: 0;` 258 | **load-background** | false | boolean | Fetch background from youtube or vimeo **THIS DEFAULTS TO FALSE** since v1.0.18. It is recommended that you provide and host your own background photo preferably as an image element with `srcset` and `loading="lazy"` for performance reasons. Works only with **YouTube** and **Vimeo**. 259 | **poster** | null | string | Provide your own background 260 | **offset** | 100 | int | showinfo:0 id deprecated since September 25, 2018. - this setting makes the video a bit larger than it's viewport to hide the info elements. This setting defaults to 100 only for **YouTube** videos. 261 | **resolution** | 16:9 | string | declare video resolution (work in progress) 262 | **pause** | false | boolean | Adds a toggle pause button (deprecated) 263 | **start-at** | 0 | int | Video starts playing at desired time in seconds 264 | **end-at** | 0 | int | Video ends playing at desired time in seconds. 0 means it will play to the end. 265 | **always-play** | false | boolean | Video will stop playing unless always-play is set to true. 266 | **volume** | 1 | float | From 0 to 1. 0 is muted, 1 is full volume. 0.5 is half volume. Sets initial volume. Setting volume doesn't work on mobile, so this setting won't have an effect on mobile. 267 | **no-cookie** | true | boolean | Disable cookies. This will prevent **YouTube** and **Vimeo** from storing information and tracking you across the web. It is set to true by default. 268 | **force-on-low-battery** | false | boolean | When mobile device is on battery saver mode, the videos will not autoplay. This setting will force autoplay on battery saver mode on user first interaction. This setting is set to false by default. Be mindful of your users and their data plans, and their battery life. 269 | **lazyloading** | false | boolean | Lazyload the ifreame/video. This setting is set to false by default. Keep in mind that the script tracks the intersecting videos and pauses them when they are not visible for the reasons of improving the performance. Use lazyloading to minimize the data usage and improve performance even more. 270 | **title** | 'Video background' | string | Title of the video for accessibility purposes. This setting is set to 'Video background' by default. Though if used as a background `aria-hidden="true"` attribute should be used on it's parent element. Setting this to false or null will remove the title attribute. 271 | 272 | Noted properties can be added as html attributes as: 273 | 274 | * **data-vbg-play-button** 275 | * **data-vbg-mute-button** 276 | * **data-vbg-autoplay** 277 | * **data-vbg-muted** 278 | * **data-vbg-loop** 279 | * **data-vbg-mobile** 280 | * **data-vbg-offset** 281 | * **data-vbg-resolution** 282 | * **data-vbg-fit-box** 283 | * **data-vbg-load-background** 284 | * **data-vbg-poster** 285 | * **data-vbg-inline-styles** 286 | * **data-vbg-start-at** 287 | * **data-vbg-end-at** 288 | * **data-vbg-always-play** 289 | * **data-vbg-volume** 290 | * **data-vbg-no-cookie** 291 | * **data-vbg-force-on-low-battery** 292 | * **data-vbg-lazyloading** 293 | 294 | **⚠️ Note:** Attribute properties will override the properties passed on initialization. Always. 295 | 296 | #### Example - Properties as HTML attributes 297 | 298 | **Vanilla** 299 | ```html 300 |
301 | 302 | 305 | ``` 306 | **jQuery** 307 | ```html 308 |
309 | 310 | 315 | ``` 316 | 317 | #### Example - Properties as JSON 318 | **Vanilla** 319 | ```html 320 |
321 | 322 | 327 | ``` 328 | **jQuery** 329 | ```html 330 |
331 | 332 | 339 | ``` 340 | 341 | 342 | ## Events 343 | 344 | * **video-background-ready** - when the video is ready to play, this event is triggered. HTML5 videos are ready to play immediately. 345 | * **video-background-time-update** - whenever the time of the video changes while video is playing, this event is triggered. The current time is available from the instance variable `event.detail.currentTime`. On Vimeo and YouTube this event is fired in 250ms intervals. 346 | * **video-background-state-change** - video changes state. The state is available from the instance variable `event.detail.currentState`. It can be: `notstarted`, `ended`, `playing`, `paused`, `buffering`. 347 | * **video-background-play** - video starts playing 348 | * **video-background-pause** - video is paused 349 | * **video-background-ended** - video ended event. Keep in mind that if loop is set to true the video will start playing from the start after this event. 350 | * **video-background-mute** - video sound is muted 351 | * **video-background-unmute** - video sound is unmuted 352 | * **video-background-volume-change** - video volume is changed. The volume is available from the instance variable `event.detail.volume`. 353 | * **video-background-resize** - when the video background is resized, this event is fired. 354 | * **video-background-destroyed** - when the video background is destroyed using the `destroy` function of the instance and reverted to pre-initialization state, this event is fired. 355 | 356 | Events bubble. If you go vanilla, you can get the video object via `event.detail` or `event.originalEvent.detail` in case of jQuery implementation. 357 | 358 | You can add listeners to the events onto the element that you've initialized the video background on. If the ID of that element is `#video-background`, you can add listeners like this: 359 | 360 | ```javascript 361 | document.querySelector('#video-background').addEventListener('video-background-ready', function(event) { 362 | console.log('video-background-ready'); // the video instance object 363 | console.log(event.detail); // the video instance object 364 | }); 365 | ``` 366 | 367 | or with jQuery: 368 | 369 | ```javascript 370 | jQuery('#video-background').on('video-background-ready', function(event) { 371 | console.log('video-background-ready'); // the video instance object 372 | console.log(event.originalEvent.detail); // the video instance object 373 | }); 374 | ``` 375 | 376 | ## Instance Methods 377 | 378 | Method | Accepts | Description 379 | -------- | ------- | ----------- 380 | **play** | - | Play the video 381 | **pause** | - | Pause the video 382 | **mute** | - | Mute the video 383 | **unmute** | - | Unmute the video 384 | **setSource** | string | Set the video source, must be a link of the same type as the original video. Meaning, for example, if the original video was a YouTube video, the new source must be a YouTube video as well. 385 | **setVolume** | float | Set the video volume. From 0 to 1. 0 is muted, 1 is full volume. 0.5 is half volume. Setting volume doesn't work on mobile, so this setting won't have an effect on mobile. 386 | **getVolume** | - | Get the video volume. From 0 to 1. 0 is muted, 1 is full volume. 0.5 is half volume. Vimeo instance will return a promise that resolves to the volume value. 387 | **seek** | int | Seek the video to a specific percentage complete. From 0 to 100. 0 is the start of the video, 100 is the end of the video. 388 | **seekTo** | int | Seek the video to a specific time in seconds. From 0 to the duration of the video in seconds. 389 | **setStartAt** | int | Set Start At seconds. From 0 to the duration of the video in seconds. 390 | **setEndAt** | int | Set End At seconds. From 0 to the duration of the video in seconds. 391 | 392 | ## Instance variables 393 | * **playing** - boolean, true if the video is playing, false if the video is not playing. Hard playing state, doesn't change on video being paused via IntersectionObserver. 394 | * **muted** - boolean, true if the video is muted, false if the video is not muted. 395 | * **isIntersecting** - boolean, true if the video is intersecting the viewport, false if the video is not intersecting the viewport. 396 | * **currentState** - the current state of the video. It can be: `notstarted`, `ended`, `playing`, `paused`, `buffering`. 397 | * **currentTime** - the current time of the video in seconds 398 | * **percentComplete** - the percentage of the video that has been played 399 | * **element** - the element that the video background is attached to 400 | * **playerElement** - the element of the video player, meaning either an iframe in case of YouTube and Vimeo, or a video element 401 | * **player** - the video player object, meaning either a YouTube or Vimeo player object, or a video element in case of HTML5 video 402 | * **type** - the type of the video, can be `youtube`, `vimeo` or `video` 403 | * **volume** - volume of the video from 0 to 1 404 | 405 | ## Factory Instance Methods 406 | 407 | Method | Accepts | Description 408 | -------- | ------- | ----------- 409 | **add** | element, parameters | Add an element to the factory instance, it will initialize the video background on that element. Parameters are optional. 410 | **get** | element or UID | Get the instance of the video background by UID or element. Returns the instance object. 411 | **destroy** | element or instance | Destroy the video background instance and revert the element to it's pre-initialization state. Accepts either the element or the instance object. 412 | **destroyAll** | - | Destroy all the instances in the index. 413 | **pauseAll** | - | Pause all the instances in the index. 414 | **playAll** | - | Play all the instances in the index. 415 | **muteAll** | - | Mute all the instances in the index. 416 | **unmuteAll** | - | Unmute all the instances in the index. 417 | **setVolumeAll** | float | Set the volume of all the instances in the index. From 0 to 1. 0 is muted, 1 is full volume. 0.5 is half volume. Setting volume doesn't work on mobile, so this setting won't have an effect on mobile. 418 | 419 | ## Factory Instance Variables 420 | * **index** - the index of all the instances of the video backgrounds. It is an object with keys being the UID of the element and values being the instance object. 421 | * **intersectionObserver** - the instance of the `IntersectionObserver` that is used to track the intersecting video backgrounds. 422 | * **resizeObserver** - the instance of the `ResizeObserver` that is used to track the resize events of the video backgrounds. If the `ResizeObserver` is not supported, the factory instance will fallback to the `window` resize event. 423 | 424 | ## Browser Support 425 | 426 | Minimum supported browsers are both desktop and mobile: 427 | * Chrome 49+ 428 | * Firefox 44+ 429 | * Safari 10+ 430 | * Opera 18+ 431 | * Edge 14+ 432 | 433 | Recommended browsers are both desktop and mobile: 434 | * Chrome 51+ 435 | * Firefox 55+ 436 | * Safari 12.1+ 437 | * Opera 38+ 438 | * Edge 17+ 439 | 440 | Tested with [BrowserStack](https://www.browserstack.com). 441 | 442 | ## Development 443 | 444 | Development setup uses **POOPS bundler** to bundle ES modules into IIFE `jquery.youtube-background.js` and `jquery.youtube-background.min.js` 445 | 446 | [POOPS](https://github.com/stamat/poops) is a simple bundler + static site builder that I've created, do give it a try and let me know what you think. It's still in early development, but it's already quite useful. 447 | 448 | To install the required package for running **POOPS**, run: 449 | 450 | ``` 451 | npm install 452 | ``` 453 | 454 | To run the server on `http://localhost:4040`, run: 455 | 456 | ``` 457 | npm run dev 458 | ``` 459 | 460 | Code will automatically be packaged into IIFE and minified while you develop, and the served page will automatically reload on changes. 461 | 462 | To just build the code, without running the local server, run: 463 | 464 | ``` 465 | npm run build 466 | ``` 467 | 468 | ### Code 469 | 470 | The code is structured like this: 471 | 472 | * **main.js** - the main entry point of the script. Used to initialize the jQuery plugin. 473 | * **video-backgrounds.js** - the main entry point of the ES6 module. It contains the factory class `VideoBackgrounds` that is used to create and index multiple instances of the video backgrounds depending on the link type: YouTube, Vimeo or video file. 474 | * **lib/super-video-background.js** - It contains the super class `SuperVideoBackground` with all of the common methods and properties for all of the video background types. This class is inherited by the `YouTubeBackground`, `VimeoBackground` and `VideoBackground` classes. 475 | * **lib/youtube-background.js** - It contains the `YouTubeBackground` class that is used to create and control YouTube video backgrounds.Inherits from `SuperVideoBackground`. 476 | * **lib/vimeo-background.js** - It contains the `VimeoBackground` class that is used to create and control Vimeo video backgrounds. Inherits from `SuperVideoBackground`. 477 | * **lib/video-background.js** - It contains the `VideoBackground` class that is used to create and control HTML5 video backgrounds. Inherits from `SuperVideoBackground`. 478 | * **lib/buttons.js** - It contains the play and pause automatic buttons and their functionality that are added to the video backgrounds. I seriously don't know why I created this in the first place. 479 | * **lib/controls.js** - Module containing externalized control classes `SeekBar`, `PlayToggle`, `MuteToggle` which tune onto the video events and use the common API, they are not bundled with the script, but are available as a standalone module exports. 480 | 481 | Tu summarize, because YouTube, Vimeo and HTML5 Video API's are different - we need a way to generalize these APIs and provide a common interface for all of them. Due to a lot of common code we have the `SuperVideoBackground` class that is inherited by the `YouTubeBackground`, `VimeoBackground` and `VideoBackground` classes. 482 | 483 | And lastly we have the `VideoBackgrounds` factory class that is used to create and index multiple instances of the video backgrounds depending on the link type: YouTube, Vimeo or video file and provide a single IntersectionObserver and ResizeObserver for all of the instances. 484 | 485 | THE END. 486 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 📺 Youtube Background 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |
23 |
24 | 25 | 26 | 27 |
28 |
29 |

Group!

30 |
31 |
32 | 33 |
34 |
35 | 36 |
37 |
38 | 39 | 40 |
41 |
42 | 43 | 44 |
45 |
46 | 47 | 48 |
49 |
50 |
51 | 52 |
53 |
54 |
55 |
56 |

Cake in a frying pan? Impossible!

57 | 58 | 59 | 60 |
61 | 62 | 63 |
64 |
65 |
66 |
67 | 68 |
69 |
70 |
71 |
72 |

Click the button to pause.

73 |

P.S. it also starts 10s into the video to skip the video intro and ends 16s into the video for shorter loops

74 | 75 |
76 | 77 | 78 |
79 |
80 |
81 |
82 | 83 |
84 |
85 |
86 |
87 |

Mute / Unmute.

88 | 89 | 90 |
91 | 92 | 93 |
94 |
95 |
96 |
97 | 98 |
99 |
101 |
102 |
103 |

Mute / Unmute.

104 | 105 | 106 | 107 |
108 | 109 | 110 |
111 |
112 |
113 |
114 | 115 |
116 |
117 |
118 |
119 |

VIMEO!

120 | 121 | 122 |
123 | 124 | 125 |
126 |
127 |
128 |
129 | 130 |
131 |
132 |
133 |
134 |

VIMEO!

135 | 136 |
137 | 138 | 139 |
140 |
141 |
142 |
143 | 144 |
145 |
146 |
147 |
148 |

VIMEO UNLISTED!

149 | 150 |
151 | 152 | 153 |
154 |
155 |
156 |
157 | 158 |
159 |
160 |
161 |
162 |

VIMEO CONTROLS!

163 | 164 |
165 | 166 | 167 |
168 |
169 |
170 |
171 | 172 |
173 |
174 |
175 |
176 |

HTML5 VIDEO!

177 | 178 | 179 |
180 | 181 | 182 |
183 |
184 |
185 |
186 | 187 |
188 |
189 |
190 |
191 |

HTML5 VIDEO!

192 | 193 |
194 | 195 | 196 |
197 |
198 |
199 |
200 | 228 |
229 |
230 | 231 | 373 | 374 | 375 | -------------------------------------------------------------------------------- /jquery.youtube-background.min.js: -------------------------------------------------------------------------------- 1 | /* youtube-background v1.1.8 | https://github.com/stamat/youtube-background | MIT License */ 2 | (()=>{function l(t){t&&(t.element.classList.add(t.stateClassName),t.element.firstChild.classList.remove(t.stateChildClassNames[0]),t.element.firstChild.classList.add(t.stateChildClassNames[1]),t.element.setAttribute("aria-pressed",!1))}function E(t){t&&(t.element.classList.remove(t.stateClassName),t.element.firstChild.classList.add(t.stateChildClassNames[0]),t.element.firstChild.classList.remove(t.stateChildClassNames[1]),t.element.setAttribute("aria-pressed",!0))}function u(t,e){const i=document.createElement("button");i.className=e.className,i.innerHTML=e.innerHtml,i.setAttribute("role","switch"),i.firstChild.classList.add(e.stateChildClassNames[0]),i.setAttribute("aria-pressed",!e.initialState),e.element=i,t.params[e.condition_parameter]===e.initialState&&l(e),i.addEventListener("click",function(s){this.classList.contains(e.stateClassName)?(E(e),t[e.actions[0]]()):(l(e),t[e.actions[1]]())}),t.buttons[e.name]={element:i,button_properties:e},t.controls_element.appendChild(i)}function w(t){if(/^\s*(true|false)\s*$/i.test(t))return t==="true"}function k(t){if(/^\s*\d+\s*$/.test(t))return parseInt(t);if(/^\s*[\d.]+\s*$/.test(t))return parseFloat(t)}function T(t){if(/^\s*\[.*\]\s*$/.test(t))try{return JSON.parse(t)}catch{}}function V(t){if(/^\s*\{.*\}\s*$/.test(t))try{return JSON.parse(t)}catch{}}function P(t){if(/^\s*\/.*\/g?i?\s*$/.test(t))try{return new RegExp(t)}catch{}}function A(t){if(/^\s*null\s*$/.test(t))return null;const e=w(t);return e!==void 0?e:k(t)||T(t)||V(t)||P(t)||t}function S(t){return Array.isArray(t)}function x(t){return typeof t=="string"}function d(t,e,i=!1){if(t=Number(t),e=Number(e),isNaN(t)||isNaN(e))throw new TypeError("Both min and max must be numbers");if(t>e&&([t,e]=[e,t]),t===e)return t;t=Math.round(t),e=Math.round(e);const s=i?N():Math.random();return Math.floor(s*(e-t+1))+t}function I(t,e){return e?parseFloat(t.toFixed(e)):parseInt(t)}function C(t,e){return!t||!e||Number.isNaN(t)||Number.isNaN(e)?0:t/e*100}function N(){if(!crypto)return Math.random();if(crypto.getRandomValues)return crypto.getRandomValues(new Uint32Array(1))[0]/4294967295}var p=/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/ ]{11})/i,m=/(?:www\.|player\.)?vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/(?:[^\/]*)\/videos\/|album\/(?:\d+)\/video\/|video\/|)(\d+)(?:[a-zA-Z0-9_\-]+)?/i,c=/\/([^\/]+\.(?:mp4|ogg|ogv|ogm|webm|avi))\s*$/i;function L(t){const e=1.7777777778;if(!t||!t.length||/16[\:x\-\/]{1}9/i.test(t))return e;const i=t.split(/\s?[\:x\-\/]{1}\s?/i);if(i.length<2)return e;const s=parseInt(i[0]),a=parseInt(i[1]);return s===0||a===0||isNaN(s)||isNaN(a)?e:s/a}function y(t,e=document){if(t instanceof Array||t instanceof NodeList)return t;if(t instanceof Element)return[t];if(e instanceof Element||e instanceof Document)return e.querySelectorAll(t);if(x(e)&&(e=y(e)),!e instanceof Array&&!e instanceof NodeList)return[];const i=[];for(const s of e)i.push(...s.querySelectorAll(t));return i}function O(t,e=1,i=0){t instanceof Element&&(t=[t]),typeof t=="string"&&(t=y(t));for(const s of t){const a=s.parentNode.offsetHeight+i,r=s.parentNode.offsetWidth+i;e>r/a?(s.style.width=a*e+"px",s.style.height=a+"px"):(s.style.width=r+"px",s.style.height=r/e+"px")}}function U(t){return/\b(BlackBerry|webOS|iPhone|IEMobile)\b/i.test(t)||/\b(Android|Windows Phone|iPad|iPod)\b/i.test(t)}function R(){return"maxTouchPoints"in navigator?navigator.maxTouchPoints>0:"matchMedia"in window?!!matchMedia("(pointer:coarse)").matches:"orientation"in window?!0:U(navigator.userAgent)}var h=class{constructor(t,e,i,s,a,r){if(!i)return;this.is_mobile=R(),this.type=a,this.id=i,this.factoryInstance=r,this.element=t,this.playerElement=null,this.uid=s,this.element.setAttribute("data-vbg-uid",s),this.buttons={},this.isIntersecting=!1,this.paused=!1,this.muted=!1,this.currentState="notstarted",this.initialPlay=!1,this.initialVolume=!1,this.volume=1,this.params={};const n={pause:!1,"play-button":!1,"mute-button":!1,autoplay:!0,muted:!0,loop:!0,mobile:!0,"load-background":!1,resolution:"16:9","inline-styles":!0,"fit-box":!1,offset:100,"start-at":0,"end-at":0,poster:null,"always-play":!1,volume:1,"no-cookie":!0,"force-on-low-battery":!1,lazyloading:!1,title:"Video background"};this.params=this.parseProperties(e,n,this.element,["data-ytbg-","data-vbg-"]),this.params.pause&&(this.params["play-button"]=this.params.pause),this.params.resolution_mod=L(this.params.resolution),this.muted=this.params.muted,this.volume=this.params.volume,this.currentTime=this.params["start-at"]||0,this.duration=this.params["end-at"]||0,this.percentComplete=0,this.params["start-at"]&&(this.percentComplete=this.timeToPercentage(this.params["start-at"])),this.buildWrapperHTML(),!(this.is_mobile&&!this.params.mobile)&&(this.params["play-button"]&&u(this,{name:"playing",className:"play-toggle",innerHtml:'',initialState:!this.paused,stateClassName:"paused",condition_parameter:"paused",stateChildClassNames:["fa-pause-circle","fa-play-circle"],actions:["play","pause"]}),this.params["mute-button"]&&u(this,{name:"muted",className:"mute-toggle",innerHtml:'',initialState:this.muted,stateClassName:"muted",condition_parameter:"muted",stateChildClassNames:["fa-volume-up","fa-volume-mute"],actions:["unmute","mute"]}))}timeToPercentage(t){if(t<=this.params["start-at"])return 0;if(t>=this.duration)return 100;if(t<=0)return 0;t-=this.params["start-at"];const e=this.duration-this.params["start-at"];return C(t,e)}percentageToTime(t){if(!this.duration)return this.params["start-at"]||0;if(t>100)return this.duration;if(t<=0)return this.params["start-at"]||0;const e=this.duration-this.params["start-at"];let i=t*e/100;return i=I(i,3),i>e&&(i=e),this.params["start-at"]&&(i+=this.params["start-at"]),i}resize(t){this.params["fit-box"]||O(t||this.playerElement,this.params.resolution_mod,this.params.offset),this.dispatchEvent("video-background-resize")}stylePlayerElement(t){t&&(this.params["inline-styles"]&&(t.style.top="50%",t.style.left="50%",t.style.transform="translateX(-50%) translateY(-50%)",t.style.position="absolute",t.style.opacity=0),this.params["fit-box"]&&(t.style.width="100%",t.style.height="100%"))}buildWrapperHTML(){const t=this.element.parentNode;this.element.classList.add("youtube-background","video-background");const e={height:"100%",width:"100%","z-index":"0",position:"absolute",overflow:"hidden",top:0,left:0,bottom:0,right:0};if(this.params["mute-button"]||(e["pointer-events"]="none"),(this.params["load-background"]||this.params.poster)&&(this.loadBackground(this.id),this.params.poster&&(e["background-image"]=`url(${this.params.poster})`),e["background-size"]="cover",e["background-repeat"]="no-repeat",e["background-position"]="center"),this.params["inline-styles"]){for(let i in e)this.element.style[i]=e[i];["absolute","fixed","relative","sticky"].indexOf(t.style.position)||(t.style.position="relative")}if(this.params["play-button"]||this.params["mute-button"]){const i=document.createElement("div");i.className="video-background-controls",i.style.position="absolute",i.style.top="10px",i.style.right="10px",i.style["z-index"]=2,this.controls_element=i,t.appendChild(i)}return this.element}loadBackground(t){this.params["load-background"]&&t&&(this.type==="youtube"&&(this.element.style["background-image"]=`url(https://img.youtube.com/vi/${t}/hqdefault.jpg)`),this.type==="vimeo"&&(this.element.style["background-image"]=`url(https://vumbnail.com/${t}.jpg)`))}destroy(){this.playerElement.remove(),this.element.classList.remove("youtube-background","video-background"),this.element.removeAttribute("data-vbg-uid"),this.element.style="",(this.params["play-button"]||this.params["mute-button"])&&this.controls_element.remove(),this.timeUpdateTimer&&clearInterval(this.timeUpdateTimer),this.dispatchEvent("video-background-destroyed")}setDuration(t){if(this.duration!==t){if(this.params["end-at"]){if(t>this.params["end-at"]){this.duration=this.params["end-at"];return}if(tt&&(this.duration=t),this.currentTime>t&&this.onVideoEnded()}dispatchEvent(t){this.element.dispatchEvent(new CustomEvent(t,{bubbles:!0,detail:this}))}shouldPlay(){return this.currentState==="ended"&&!this.params.loop?!1:!!(this.params["always-play"]&&this.currentState!=="playing"||this.isIntersecting&&this.params.autoplay&&this.currentState!=="playing")}mobileLowBatteryAutoplayHack(){if(!this.params["force-on-low-battery"]||!this.is_mobile&&this.params.mobile)return;const t=function(){!this.initialPlay&&this.params.autoplay&&this.params.muted&&(this.softPlay(),!this.isIntersecting&&!this.params["always-play"]&&this.softPause())};document.addEventListener("touchstart",t.bind(this),{once:!0})}parseProperties(t,e,i,s){let a={};if(!t)a=e;else for(let r in e)a[r]=t.hasOwnProperty(r)?t[r]:e[r];if(!i)return a;for(let r in a){let n;if(S(s))for(let o=0;o=this.duration){this.currentState="ended",this.dispatchEvent("video-background-state-change"),this.onVideoEnded(),this.stopTimeUpdateTimer();return}this.dispatchEvent("video-background-time-update")}}onVideoPlayerReady(){this.mobileLowBatteryAutoplayHack(),this.params.autoplay&&(this.params["always-play"]||this.isIntersecting)&&(this.params["start-at"]&&this.seekTo(this.params["start-at"]),this.player.playVideo()),this.setDuration(this.player.getDuration()),this.dispatchEvent("video-background-ready")}onVideoStateChange(t){this.currentState=this.convertState(t.data),this.currentState==="ended"&&this.onVideoEnded(),this.currentState==="notstarted"&&this.params.autoplay&&(this.seekTo(this.params["start-at"]),this.player.playVideo()),this.currentState==="playing"&&this.onVideoPlay(),this.currentState==="paused"&&this.onVideoPause(),this.dispatchEvent("video-background-state-change")}onVideoPlay(){this.initialPlay||(this.initialPlay=!0,this.playerElement.style.opacity=1);const t=this.player.getCurrentTime();this.params["start-at"]&&t=this.duration&&this.seekTo(this.params["start-at"]),this.duration||this.setDuration(this.player.getDuration()),this.dispatchEvent("video-background-play"),this.startTimeUpdateTimer()}onVideoPause(){this.stopTimeUpdateTimer(),this.dispatchEvent("video-background-pause")}onVideoEnded(){if(this.dispatchEvent("video-background-ended"),!this.params.loop)return this.pause();this.seekTo(this.params["start-at"]),this.player.playVideo()}seek(t){this.seekTo(this.percentageToTime(t),!0)}seekTo(t,e=!0){this.player&&(this.player.seekTo(t,e),this.dispatchEvent("video-background-seeked"))}softPause(){!this.player||this.currentState==="paused"||(this.stopTimeUpdateTimer(),this.player.pauseVideo())}softPlay(){!this.player||this.currentState==="playing"||this.player.playVideo()}play(){this.player&&(this.paused=!1,this.player.playVideo())}pause(){this.player&&(this.paused=!0,this.stopTimeUpdateTimer(),this.player.pauseVideo())}unmute(){this.player&&(this.muted=!1,this.initialVolume||(this.initialVolume=!0,this.setVolume(this.params.volume)),this.player.unMute(),this.dispatchEvent("video-background-unmute"))}mute(){this.player&&(this.muted=!0,this.player.mute(),this.dispatchEvent("video-background-mute"))}getVolume(){if(this.player)return this.player.getVolume()/100}setVolume(t){this.player&&(this.volume=t,this.player.setVolume(t*100),this.dispatchEvent("video-background-volume-change"))}},g=class extends h{constructor(t,e,i,s,a){super(t,e,i.id,s,"vimeo",a),i&&(this.unlisted=i.unlisted,!(this.is_mobile&&!this.params.mobile)&&(this.injectScript(),this.player=null,this.injectPlayer(),this.initVimeoPlayer()))}injectScript(){const t="https://player.vimeo.com/api/player.js";if(window.hasOwnProperty("Vimeo")||document.querySelector(`script[src="${t}"]`))return;const e=document.createElement("script");e.async=!0,window.hasOwnProperty("onVimeoIframeAPIReady")&&typeof window.onVimeoIframeAPIReady=="function"&&e.addEventListener("load",()=>{window.onVimeoIframeAPIReady()}),e.src=t;const i=document.getElementsByTagName("script")[0];i.parentNode.insertBefore(e,i)}initVimeoPlayer(){!window.hasOwnProperty("Vimeo")||this.player!==null||(this.player=new Vimeo.Player(this.playerElement),this.player.on("loaded",this.onVideoPlayerReady.bind(this)),this.player.on("ended",this.onVideoEnded.bind(this)),this.player.on("play",this.onVideoPlay.bind(this)),this.player.on("pause",this.onVideoPause.bind(this)),this.player.on("bufferstart",this.onVideoBuffering.bind(this)),this.player.on("timeupdate",this.onVideoTimeUpdate.bind(this)),this.volume!==1&&!this.muted&&this.setVolume(this.volume))}onVideoError(t){console.error(t)}generatePlayerElement(){const t=document.createElement("iframe");return this.params.title&&t.setAttribute("title",this.params.title),t.setAttribute("frameborder",0),t.setAttribute("allow","autoplay; mute"),this.params.lazyloading&&t.setAttribute("loading","lazy"),t}generateSrcURL(t,e){e=e?`h=${e}&`:"";let i=`https://player.vimeo.com/video/${t}?${e}background=1&controls=0`;return this.params.muted&&(i+="&muted=1"),this.params.autoplay&&(this.params["always-play"]||this.isIntersecting)&&(i+="&autoplay=1"),this.params.loop&&(i+="&loop=1&autopause=0"),this.params["no-cookie"]&&(i+="&dnt=1"),this.params["start-at"]&&(i+="#t="+this.params["start-at"]+"s"),i}injectPlayer(){this.playerElement=this.generatePlayerElement(),this.src=this.generateSrcURL(this.id,this.unlisted),this.playerElement.src=this.src,this.playerElement.id=this.uid,this.stylePlayerElement(this.playerElement),this.element.appendChild(this.playerElement),this.resize(this.playerElement)}updateState(t){this.currentState=t,this.dispatchEvent("video-background-state-change")}setSource(t){const e=t.match(m);!e||!e.length||(this.id=e[1],this.src=this.generateSrcURL(this.id),this.playerElement.src=this.src,this.element.hasAttribute("data-vbg")&&this.element.setAttribute("data-vbg",this.src),this.element.hasAttribute("data-ytbg")&&this.element.setAttribute("data-ytbg",this.src),this.loadBackground(this.id))}onVideoPlayerReady(){this.mobileLowBatteryAutoplayHack(),this.params["start-at"]&&this.seekTo(this.params["start-at"]),this.params.autoplay&&(this.params["always-play"]||this.isIntersecting)&&this.player.play(),this.player.getDuration().then(t=>{this.setDuration(t)}),this.dispatchEvent("video-background-ready")}onVideoEnded(){if(this.updateState("ended"),this.dispatchEvent("video-background-ended"),!this.params.loop)return this.pause();this.seekTo(this.params["start-at"]),this.updateState("playing"),this.dispatchEvent("video-background-play")}onVideoTimeUpdate(t){this.currentTime=t.seconds,this.percentComplete=this.timeToPercentage(t.seconds),this.dispatchEvent("video-background-time-update"),this.setDuration(t.duration),this.params["end-at"]&&this.duration&&t.seconds>=this.duration&&this.onVideoEnded()}onVideoBuffering(){this.updateState("buffering")}onVideoPlay(t){if(this.setDuration(t.duration),!this.initialPlay&&(this.initialPlay=!0,this.playerElement.style.opacity=1,this.player.setLoop(this.params.loop),!(this.params.autoplay&&(this.params["always-play"]||this.isIntersecting))))return this.player.pause();const e=t.seconds;this.params["start-at"]&&e=this.duration&&this.seekTo(this.params["start-at"]),this.updateState("playing"),this.dispatchEvent("video-background-play")}onVideoPause(){this.updateState("paused"),this.dispatchEvent("video-background-pause")}seek(t){this.seekTo(this.percentageToTime(t))}seekTo(t){this.player&&(this.player.setCurrentTime(t),this.dispatchEvent("video-background-seeked"))}softPause(){!this.player||this.currentState==="paused"||this.player.pause()}softPlay(){!this.player||this.currentState==="playing"||this.player.play()}play(){this.player&&(this.paused=!1,this.player.play())}pause(){this.player&&(this.paused=!0,this.player.pause())}unmute(){this.player&&(this.muted=!1,this.initialVolume||(this.initialVolume=!0,this.setVolume(this.params.volume)),this.player.setMuted(!1),this.dispatchEvent("video-background-unmute"))}mute(){this.player&&(this.muted=!0,this.player.setMuted(!0),this.dispatchEvent("video-background-mute"))}getVolume(){if(this.player)return this.player.getVolume()}setVolume(t){this.player&&(this.volume=t,this.player.setVolume(t),this.dispatchEvent("video-background-volume-change"))}},B=class extends h{constructor(t,e,i,s,a){super(t,e,i.link,s,"video",a),!(!i||!i.link)&&(this.is_mobile&&!this.params.mobile||(this.src=i.link,this.ext=/(?:\.([^.]+))?$/.exec(i.id)[1],this.uid=s,this.element.setAttribute("data-vbg-uid",s),this.player=null,this.buttons={},this.MIME_MAP={ogv:"video/ogg",ogm:"video/ogg",ogg:"video/ogg",avi:"video/avi",mp4:"video/mp4",webm:"video/webm",m4v:"video/x-m4v",mov:"video/quicktime",qt:"video/quicktime"},this.mime=this.MIME_MAP[this.ext.toLowerCase()],this.injectPlayer(),this.mobileLowBatteryAutoplayHack(),this.dispatchEvent("video-background-ready")))}generatePlayerElement(){const t=document.createElement("video");return this.params.title&&t.setAttribute("title",this.params.title),t.setAttribute("playsinline",""),this.params.loop&&t.setAttribute("loop",""),this.params.autoplay&&(this.params["always-play"]||this.isIntersecting)&&(t.setAttribute("autoplay",""),t.autoplay=!0),this.muted&&(t.setAttribute("muted",""),t.muted=!0),this.params.lazyloading&&t.setAttribute("loading","lazy"),t}injectPlayer(){this.player=this.generatePlayerElement(),this.playerElement=this.player,this.volume!==1&&!this.muted&&this.setVolume(this.volume),this.playerElement.setAttribute("id",this.uid),this.stylePlayerElement(this.playerElement),this.player.addEventListener("loadedmetadata",this.onVideoLoadedMetadata.bind(this)),this.player.addEventListener("durationchange",this.onVideoLoadedMetadata.bind(this)),this.player.addEventListener("canplay",this.onVideoCanPlay.bind(this)),this.player.addEventListener("timeupdate",this.onVideoTimeUpdate.bind(this)),this.player.addEventListener("play",this.onVideoPlay.bind(this)),this.player.addEventListener("pause",this.onVideoPause.bind(this)),this.player.addEventListener("waiting",this.onVideoBuffering.bind(this)),this.player.addEventListener("ended",this.onVideoEnded.bind(this)),this.element.appendChild(this.playerElement);const t=document.createElement("source");t.setAttribute("src",this.src),t.setAttribute("type",this.mime),this.playerElement.appendChild(t),this.resize(this.playerElement)}updateState(t){this.currentState=t,this.dispatchEvent("video-background-state-change")}setSource(t){const e=t.match(c);if(!e||!e.length)return;this.id=e[1],this.ext=/(?:\.([^.]+))?$/.exec(this.id)[1],this.mime=this.MIME_MAP[this.ext.toLowerCase()],this.playerElement.innerHTML="";const i=document.createElement("source");i.setAttribute("src",t),i.setAttribute("type",this.mime),this.playerElement.appendChild(i),this.src=t,this.element.hasAttribute("data-vbg")&&this.element.setAttribute("data-vbg",this.src),this.element.hasAttribute("data-ytbg")&&this.element.setAttribute("data-ytbg",this.src)}onVideoLoadedMetadata(){this.setDuration(this.player.duration)}onVideoCanPlay(){this.setDuration(this.player.duration)}onVideoTimeUpdate(){this.currentTime=this.player.currentTime,this.percentComplete=this.timeToPercentage(this.player.currentTime),this.dispatchEvent("video-background-time-update"),this.params["end-at"]&&this.currentTime>=this.duration&&this.onVideoEnded()}onVideoPlay(){this.initialPlay||(this.initialPlay=!0,this.playerElement.style.opacity=1);const t=this.player.currentTime;this.params["start-at"]&&t<=this.params["start-at"]&&this.seekTo(this.params["start-at"]),this.duration&&t>=this.duration&&this.seekTo(this.params["start-at"]),this.updateState("playing"),this.dispatchEvent("video-background-play")}onVideoPause(){this.updateState("paused"),this.dispatchEvent("video-background-pause")}onVideoEnded(){if(this.updateState("ended"),this.dispatchEvent("video-background-ended"),!this.params.loop)return this.pause();this.seekTo(this.params["start-at"]),this.onVideoPlay()}onVideoBuffering(){this.updateState("buffering")}seek(t){this.seekTo(this.percentageToTime(t))}seekTo(t){if(this.player){if(this.player.hasOwnProperty("fastSeek")){this.player.fastSeek(t);return}this.player.currentTime=t,this.dispatchEvent("video-background-seeked")}}softPause(){!this.player||this.currentState==="paused"||this.player.pause()}softPlay(){!this.player||this.currentState==="playing"||this.player.play()}play(){this.player&&(this.paused=!1,this.player.play())}pause(){this.player&&(this.paused=!0,this.player.pause())}unmute(){this.player&&(this.muted=!1,this.player.muted=!1,this.initialVolume||(this.initialVolume=!0,this.setVolume(this.params.volume)),this.dispatchEvent("video-background-unmute"))}mute(){this.player&&(this.muted=!0,this.player.muted=!0,this.dispatchEvent("video-background-mute"))}getVolume(){if(this.player)return this.player.volume}setVolume(t){this.player&&(this.volume=t,this.player.volume=t,this.dispatchEvent("video-background-volume-change"))}},b=class{constructor(t,e){this.elements=t,this.elements instanceof Element&&(this.elements=[this.elements]),typeof this.elements=="string"&&(this.elements=document.querySelectorAll(t)),this.index={};const i=this;if(this.intersectionObserver=null,"IntersectionObserver"in window&&(this.intersectionObserver=new IntersectionObserver(function(s){s.forEach(function(a){const r=a.target.getAttribute("data-vbg-uid");if(r&&i.index.hasOwnProperty(r)&&a.isIntersecting){i.index[r].isIntersecting=!0;try{i.index[r].player&&!i.index[r].paused&&i.index[r].softPlay()}catch{}}else{i.index[r].isIntersecting=!1;try{i.index[r].player&&i.index[r].softPause()}catch{}}})})),this.resizeObserver=null,"ResizeObserver"in window?this.resizeObserver=new ResizeObserver(function(s){s.forEach(function(a){const r=a.target.getAttribute("data-vbg-uid");r&&i.index.hasOwnProperty(r)&&window.requestAnimationFrame(()=>i.index[r].resize())})}):window.addEventListener("resize",function(){for(let s in i.index)window.requestAnimationFrame(()=>i.index[s].resize(i.index[s].playerElement))}),this.initPlayers(),!(!this.elements||!this.elements.length)){for(let s=0;s", 23 | "contributors": [], 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/stamat/youtube-background/issues" 27 | }, 28 | "devDependencies": { 29 | "poops": "^1.0.18" 30 | }, 31 | "scripts": { 32 | "dev": "poops", 33 | "build": "poops -b" 34 | }, 35 | "dependencies": { 36 | "book-of-spells": "^1.0.18" 37 | } 38 | } -------------------------------------------------------------------------------- /poops.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": [{ 3 | "in": "src/main.js", 4 | "out": "jquery.youtube-background.js", 5 | "options": { 6 | "sourcemap": true, 7 | "minify": true, 8 | "format": "iife", 9 | "target": "es2019" 10 | } 11 | }, 12 | { 13 | "in": "src/experimental.js", 14 | "out": "youtube-background-experimental.js", 15 | "options": { 16 | "sourcemap": true, 17 | "minify": true, 18 | "format": "iife", 19 | "target": "es2019" 20 | } 21 | }], 22 | "banner": "/* {{ name }} v{{ version }} | {{ homepage }} | {{ license }} License */", 23 | "serve" : { 24 | "port": 4040, 25 | "base": "/" 26 | }, 27 | "livereload": true, 28 | "watch": [ 29 | "src" 30 | ], 31 | "includePaths": [ 32 | "node_modules" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /script/build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | npx poops -b 4 | -------------------------------------------------------------------------------- /script/publish: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * This script is used to publish a new version of the package. 5 | * It can automatically increment the patch version if no version is specified in the package.json and commit the changes. 6 | * It can also automatically tag and push the tags. 7 | * Using GH CLI, it can also create a release. 8 | * 9 | * Fucking awesome, right? 10 | * 11 | * With love, @stamat 12 | */ 13 | const readline = require('readline') 14 | const { exec } = require('child_process') 15 | const fs = require('fs') 16 | const path = require('path') 17 | const argVersion = process.argv[2] 18 | const packageJson = require(path.join(process.cwd(), './package.json')) 19 | 20 | function question(prompt) { 21 | const rl = readline.createInterface({ 22 | input: process.stdin, 23 | output: process.stdout 24 | }) 25 | 26 | return new Promise((resolve) => { 27 | rl.question(prompt, (answer) => { 28 | rl.close() 29 | resolve(answer) 30 | }) 31 | }) 32 | } 33 | 34 | function isValidVersion(version) { 35 | return /^(\d+)\.(\d+)\.(\d+)(?:-([\w-]+(?:\.[\w-]+)*))?(?:\+([\w-]+(?:\.[\w-]+)*))?$/.test(version) 36 | } 37 | 38 | function incrementPatchVersion(version) { 39 | const parts = version.split('.') 40 | const patch = parseInt(parts[2]) + 1 41 | return `${parts[0]}.${parts[1]}.${patch}` 42 | } 43 | 44 | function runPromise(child) { 45 | child.stdout.on('data', (data) => { 46 | console.log(data.toString()) 47 | }) 48 | 49 | child.stderr.on('data', (data) => { 50 | console.log(data.toString()) 51 | }) 52 | 53 | return new Promise(function(resolve, reject) { 54 | child.addListener('error', reject) 55 | child.addListener('exit', resolve) 56 | }) 57 | } 58 | 59 | async function run(cmd, exitOnError = true) { 60 | console.log(cmd) 61 | try { 62 | await runPromise(exec(cmd)) 63 | } catch (e) { 64 | if (exitOnError) process.exit(1) 65 | } 66 | } 67 | 68 | async function publish(version) { 69 | packageJson.version = version 70 | fs.writeFileSync('package.json', JSON.stringify(packageJson, null, 2)) 71 | 72 | await run('git add package.json') 73 | await run(`git commit -m "Bump version to ${version}"`) 74 | 75 | if (fs.existsSync(path.join(process.cwd(), 'script/build'))) { 76 | await run('script/build') 77 | await run('git add .') 78 | await run(`git commit -m "Build version ${version}"`) 79 | } 80 | 81 | await run(`git tag v${version}`) 82 | await run('git push') 83 | await run('git push --tags') 84 | 85 | const publish = await question('Do you want to create a GitHub release? (y/n): ') 86 | if (publish === 'y') { 87 | let notesArg = '' 88 | const notes = await question('Enter notes for the release (optional): ') 89 | if (notes.trim() !== '') { 90 | notesArg = ` --notes "${notes}"` 91 | } 92 | await run(`gh release create v${version} --title "v${version}"${notesArg} --generate-notes --latest`) 93 | await run('npm publish') 94 | } else { 95 | await run('npm publish') 96 | } 97 | } 98 | 99 | async function init() { 100 | if (!packageJson.version) { 101 | console.log('No version found in package.json') 102 | process.exit(1) 103 | } 104 | 105 | if (argVersion && !isValidVersion(argVersion)) { 106 | console.log('Invalid version: ', argVersion) 107 | console.log('Current version: ', packageJson.version) 108 | process.exit(1) 109 | } 110 | 111 | if (argVersion && packageJson.version === argVersion) { 112 | console.log('Version is already ', argVersion) 113 | process.exit(1) 114 | } 115 | 116 | if (!argVersion) { 117 | console.log('Current version: ', packageJson.version) 118 | const version = incrementPatchVersion(packageJson.version) 119 | const answer = await question(`Do you want to increment the version to ${version}? (y/n) `) 120 | if (answer === 'y') { 121 | publish(version) 122 | } else { 123 | const desiredVersion = await question(`Enter the desired version: (or press enter to use ${packageJson.version})`) 124 | if (desiredVersion.trim() === '') { 125 | publish(packageJson.version) 126 | } else { 127 | if (!isValidVersion(desiredVersion)) { 128 | console.log('Invalid version: ', desiredVersion) 129 | process.exit(1) 130 | } 131 | publish(desiredVersion) 132 | } 133 | } 134 | } else { 135 | publish(argVersion) 136 | } 137 | } 138 | init() 139 | -------------------------------------------------------------------------------- /script/server: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | npx poops 4 | -------------------------------------------------------------------------------- /src/experimental.js: -------------------------------------------------------------------------------- 1 | import { SeekBar, PlayToggle, MuteToggle, VideoBackgroundGroup, VideoBackgroundGroups } from "./lib/controls"; 2 | 3 | window.SeekBar = SeekBar; 4 | window.PlayToggle = PlayToggle; 5 | window.MuteToggle = MuteToggle; 6 | window.VideoBackgroundGroup = VideoBackgroundGroup; 7 | window.VideoBackgroundGroups = VideoBackgroundGroups; 8 | -------------------------------------------------------------------------------- /src/lib/buttons.js: -------------------------------------------------------------------------------- 1 | 2 | function buttonOn(buttonObj) { 3 | if (!buttonObj) return; 4 | buttonObj.element.classList.add(buttonObj.stateClassName); 5 | buttonObj.element.firstChild.classList.remove(buttonObj.stateChildClassNames[0]); 6 | buttonObj.element.firstChild.classList.add(buttonObj.stateChildClassNames[1]); 7 | buttonObj.element.setAttribute('aria-pressed', false); 8 | } 9 | 10 | function buttonOff(buttonObj) { 11 | if (!buttonObj) return; 12 | buttonObj.element.classList.remove(buttonObj.stateClassName); 13 | buttonObj.element.firstChild.classList.add(buttonObj.stateChildClassNames[0]); 14 | buttonObj.element.firstChild.classList.remove(buttonObj.stateChildClassNames[1]); 15 | buttonObj.element.setAttribute('aria-pressed', true); 16 | } 17 | 18 | export function generateActionButton(obj, props) { 19 | const btn = document.createElement('button'); 20 | btn.className = props.className; 21 | btn.innerHTML = props.innerHtml; 22 | btn.setAttribute('role', 'switch'); 23 | btn.firstChild.classList.add(props.stateChildClassNames[0]); 24 | btn.setAttribute('aria-pressed', !props.initialState); 25 | props.element = btn; 26 | 27 | if (obj.params[props.condition_parameter] === props.initialState) { 28 | buttonOn(props); 29 | } 30 | 31 | btn.addEventListener('click', function(e) { 32 | if (this.classList.contains(props.stateClassName)) { 33 | buttonOff(props); 34 | obj[props.actions[0]](); 35 | } else { 36 | buttonOn(props); 37 | obj[props.actions[1]](); 38 | } 39 | }); 40 | 41 | obj.buttons[props.name] = { 42 | element: btn, 43 | button_properties: props 44 | }; 45 | 46 | obj.controls_element.appendChild(btn); 47 | }; 48 | -------------------------------------------------------------------------------- /src/lib/controls.js: -------------------------------------------------------------------------------- 1 | export class SeekBar { 2 | constructor(element, vbgInstance) { 3 | this.lock = false; 4 | if (!element) return; 5 | this.element = element; 6 | if (this.element.hasAttribute('data-target-uid')) return; 7 | this.progressElem = this.element.querySelector('.js-seek-bar-progress'); 8 | this.inputElem = this.element.querySelector('.js-seek-bar'); 9 | this.targetSelector = this.element.getAttribute('data-target'); 10 | if (this.targetSelector) this.targetElem = document.querySelector(this.targetSelector); 11 | if (!this.targetSelector && vbgInstance) this.targetElem = vbgInstance.element; 12 | if (!this.targetElem) return; 13 | 14 | if (vbgInstance) this.setVBGInstance(vbgInstance); 15 | 16 | this.targetElem.addEventListener('video-background-time-update', this.onTimeUpdate.bind(this)); 17 | this.targetElem.addEventListener('video-background-play', this.onReady.bind(this)); 18 | this.targetElem.addEventListener('video-background-ready', this.onReady.bind(this)); 19 | this.targetElem.addEventListener('video-background-destroyed', this.onDestroyed.bind(this)); 20 | 21 | this.inputElem.addEventListener('input', this.onInput.bind(this)); 22 | this.inputElem.addEventListener('change', this.onChange.bind(this)); 23 | } 24 | 25 | setVBGInstance(vbgInstance) { 26 | if (this.vbgInstance) return; 27 | this.vbgInstance = vbgInstance; 28 | this.element.setAttribute('data-target-uid', vbgInstance.uid); 29 | } 30 | 31 | onReady(event) { 32 | this.setVBGInstance(event.detail); 33 | } 34 | 35 | onTimeUpdate(event) { 36 | this.setVBGInstance(event.detail); 37 | if (!this.lock) requestAnimationFrame(() => this.setProgress(this.vbgInstance.percentComplete)); 38 | } 39 | 40 | onDestroyed(event) { 41 | this.vbgInstance = null; 42 | requestAnimationFrame(() => this.setProgress(0)); 43 | } 44 | 45 | onInput(event) { 46 | this.lock = true; 47 | requestAnimationFrame(() => this.setProgress(event.target.value)); 48 | } 49 | 50 | onChange(event) { 51 | this.lock = false; 52 | requestAnimationFrame(() => this.setProgress(event.target.value)); 53 | if (this.vbgInstance) { 54 | this.vbgInstance.seek(event.target.value); 55 | if (this.vbgInstance.playerElement && this.vbgInstance.playerElement.style.opacity === 0) this.vbgInstance.playerElement.style.opacity = 1; 56 | } 57 | } 58 | 59 | setProgress(value) { 60 | if (this.progressElem) this.progressElem.value = value; 61 | if (this.inputElem) this.inputElem.value = value; 62 | } 63 | } 64 | 65 | export class VideoBackgroundGroup { 66 | constructor(selector, videoBackgroundSelector, videoBackgroundFactoryInstance) { 67 | this.element = selector; 68 | if (typeof this.element === 'string') this.element = document.querySelector(selector); 69 | if (!this.element) return; 70 | this.elements = this.element.querySelectorAll(videoBackgroundSelector || '[data-vbg]'); 71 | if (!this.elements.length) return; 72 | 73 | this.videoBackgroundFactoryInstance = videoBackgroundFactoryInstance; 74 | this.stack = []; 75 | this.map = new Map(); 76 | this.current = 0; 77 | this.currentElement = null; 78 | this.currentInstance = null; 79 | 80 | this.playing = false; 81 | this.muted = true; 82 | 83 | for (let i = 0; i < this.elements.length; i++) { 84 | const element = this.elements[i]; 85 | if (!element.hasAttribute('data-vbg-uid') && this.videoBackgroundFactoryInstance) this.videoBackgroundFactoryInstance.add(element); 86 | this.stack.push(element); 87 | this.map.set(element, i); 88 | 89 | if (i === 0) { 90 | this.current = 0; 91 | this.currentElement = element; 92 | if (this.videoBackgroundFactoryInstance) this.currentInstance = this.videoBackgroundFactoryInstance.get(element); 93 | } 94 | element.addEventListener('video-background-ended', this.onVideoEnded.bind(this)); 95 | element.addEventListener('video-background-seeked', this.onVideoSeeked.bind(this)); 96 | element.addEventListener('video-background-pause', this.onVideoPause.bind(this)); 97 | element.addEventListener('video-background-ready', this.onVideoReady.bind(this)); 98 | element.addEventListener('video-background-state-change', this.setVideoBackgroundFactoryInstance.bind(this), { once: true }); 99 | element.addEventListener('video-background-time-update', this.setVideoBackgroundFactoryInstance.bind(this), { once: true }); 100 | } 101 | } 102 | 103 | setVideoBackgroundFactoryInstance(event) { 104 | if (this.videoBackgroundFactoryInstance) return; 105 | this.videoBackgroundFactoryInstance = event.detail.factoryInstance; 106 | if (!this.currentInstance) this.currentInstance = this.videoBackgroundFactoryInstance.get(this.currentElement); 107 | } 108 | 109 | onVideoReady(event) { 110 | if (this.stack[this.current] !== event.detail.element) return; 111 | this.setVideoBackgroundFactoryInstance(event); 112 | const videoBackground = event.detail; 113 | if (videoBackground.params.muted) this.muted = true; 114 | if (!videoBackground.isIntersecting) return; 115 | if (!videoBackground.params.autoplay) return; 116 | this.playing = true; 117 | if (videoBackground.currentState === 'playing') return; 118 | videoBackground.softPlay(); 119 | } 120 | 121 | onVideoPause(event) {; 122 | this.setVideoBackgroundFactoryInstance(event); 123 | const stackIndex = this.map.get(event.detail.element); 124 | if (stackIndex === this.current) return; 125 | } 126 | 127 | levelSeekBars() { 128 | for (let i = 0; i < this.stack.length; i++) { 129 | if (i === this.current) continue; 130 | const seekBarElem = this.getSeekBar(this.videoBackgroundFactoryInstance.get(this.stack[i])); 131 | if (!seekBarElem) continue; 132 | if (i < this.current) { 133 | this.setProgress(seekBarElem, 100); 134 | } else { 135 | this.setProgress(seekBarElem, 0); 136 | } 137 | } 138 | } 139 | 140 | getSeekBar(currentInstance) { 141 | if (!currentInstance) return; 142 | const uid = currentInstance.uid; 143 | const element = document.querySelector(`.js-seek-bar-wrap[data-target-uid="${uid}"]`); 144 | if (!element) return; 145 | return element; 146 | } 147 | 148 | setProgress(seekBarElem, value) { 149 | if (!seekBarElem) return; 150 | const progressElem = seekBarElem.querySelector('.js-seek-bar-progress'); 151 | const inputElem = seekBarElem.querySelector('.js-seek-bar'); 152 | if (progressElem) progressElem.value = value; 153 | if (inputElem) inputElem.value = value; 154 | } 155 | 156 | onVideoSeeked(event) { 157 | const current = this.map.get(event.detail.element); 158 | if (this.current !== current) this.setCurrent(current, true); 159 | } 160 | 161 | setCurrent(index, seek) { 162 | const previous = this.current; 163 | if (index >= this.stack.length) index = 0; 164 | if (index < 0) index = this.stack.length - 1; 165 | const previousInstance = this.videoBackgroundFactoryInstance.get(this.stack[previous]); 166 | this.current = index; 167 | this.currentInstance = this.videoBackgroundFactoryInstance.get(this.stack[this.current]); 168 | this.currentElement = this.stack[this.current]; 169 | 170 | this.stack[previous].style.display = 'none'; 171 | this.currentElement.style.display = 'block'; 172 | 173 | if (!seek) { 174 | const seekBarElem = this.getSeekBar(this.currentInstance); 175 | if (seekBarElem) this.setProgress(seekBarElem, 0); 176 | this.currentInstance.seek(0); 177 | } 178 | 179 | setTimeout(() => { 180 | if (this.currentInstance.currentState !== 'playing') this.currentInstance.play(); 181 | }, 100); 182 | if (previousInstance && previousInstance.currentState !== 'paused') previousInstance.pause(); 183 | 184 | setTimeout(this.levelSeekBars.bind(this), 100); 185 | 186 | if (index >= this.stack.length) this.dispatchEvent('video-background-group-forward-rewind'); 187 | if (index < 0) this.dispatchEvent('video-background-group-backward-rewind'); 188 | } 189 | 190 | dispatchEvent(name) { 191 | this.element.dispatchEvent(new CustomEvent(name, { bubbles: true, detail: this })); 192 | } 193 | 194 | onVideoEnded(event) { 195 | if (event.detail.element !== this.currentElement) return; 196 | this.next(); 197 | } 198 | 199 | next() { 200 | this.setCurrent(this.current + 1); 201 | this.dispatchEvent('video-background-group-next'); 202 | } 203 | 204 | prev() { 205 | this.setCurrent(this.current - 1); 206 | this.dispatchEvent('video-background-group-previous'); 207 | } 208 | 209 | unmute() { 210 | for (let i = 0; i < this.stack.length; i++) { 211 | const instance = this.videoBackgroundFactoryInstance.get(this.stack[i]); 212 | if (!instance) continue; 213 | instance.unmute(); 214 | } 215 | 216 | this.muted = false; 217 | this.dispatchEvent('video-background-group-umnute'); 218 | } 219 | 220 | mute() { 221 | for (let i = 0; i < this.stack.length; i++) { 222 | const instance = this.videoBackgroundFactoryInstance.get(this.stack[i]); 223 | if (!instance) continue; 224 | instance.mute(); 225 | } 226 | 227 | this.muted = true; 228 | this.dispatchEvent('video-background-group-mute'); 229 | } 230 | 231 | pause() { 232 | this.currentInstance.pause(); 233 | this.playing = false; 234 | this.dispatchEvent('video-background-group-pause'); 235 | } 236 | 237 | play() { 238 | this.currentInstance.play(); 239 | this.playing = true; 240 | this.dispatchEvent('video-background-group-play'); 241 | } 242 | 243 | destroy() { 244 | for (let i = 0; i < this.elements.length; i++) { 245 | const element = this.elements[i]; 246 | element.removeEventListener('video-background-ended', this.onVideoEnded.bind(this)); 247 | element.removeEventListener('video-background-seeked', this.onVideoSeeked.bind(this)); 248 | element.removeEventListener('video-background-pause', this.onVideoPause.bind(this)); 249 | element.removeEventListener('video-background-ready', this.onVideoReady.bind(this)); 250 | element.removeEventListener('video-background-state-change', this.setVideoBackgroundFactoryInstance.bind(this)); 251 | element.removeEventListener('video-background-time-update', this.setVideoBackgroundFactoryInstance.bind(this)); 252 | } 253 | } 254 | } 255 | 256 | export class PlayToggle { 257 | constructor(playToggleElem, vbgInstance) { 258 | if (!playToggleElem) return; 259 | 260 | this.element = playToggleElem; 261 | this.targetSelector = this.element.getAttribute('data-target'); 262 | 263 | if (!this.targetSelector) return; 264 | this.active = false; 265 | 266 | if (this.element.hasAttribute('aria-pressed')) { 267 | this.active = this.element.getAttribute('aria-pressed') === 'true'; 268 | } else { 269 | this.element.setAttribute('aria-pressed', this.active); 270 | } 271 | 272 | this.element.setAttribute('role', 'switch'); 273 | 274 | this.targetElem = document.querySelector(this.targetSelector); 275 | if (!this.targetElem) return; 276 | 277 | if (vbgInstance) this.vbgInstance = vbgInstance; 278 | 279 | this.targetElem.addEventListener('video-background-ready', this.onReady.bind(this)); 280 | this.targetElem.addEventListener('video-background-state-change', this.onStateChange.bind(this)); 281 | this.targetElem.addEventListener('video-background-play', this.onPlay.bind(this)); 282 | this.targetElem.addEventListener('video-background-pause', this.onPause.bind(this)); 283 | this.targetElem.addEventListener('video-background-destroyed', this.onDestroyed.bind(this)); 284 | 285 | this.element.addEventListener('click', this.onClick.bind(this)); 286 | } 287 | 288 | onReady(event) { 289 | this.vbgInstance = event.detail; 290 | } 291 | 292 | onStateChange(event) { 293 | if (!this.vbgInstance) this.vbgInstance = event.detail; 294 | this.active = this.vbgInstance.currentState === 'playing' || this.vbgInstance.currentState === 'buffering'; 295 | this.element.setAttribute('aria-pressed', this.active); 296 | } 297 | 298 | onPlay(event) { 299 | if (!this.vbgInstance) this.vbgInstance = event.detail; 300 | this.active = true; 301 | this.element.setAttribute('aria-pressed', this.active); 302 | } 303 | 304 | onPause(event) { 305 | if (!this.vbgInstance) this.vbgInstance = event.detail; 306 | this.active = false; 307 | this.element.setAttribute('aria-pressed', this.active); 308 | } 309 | 310 | onDestroyed(event) { 311 | this.vbgInstance = null; 312 | this.active = false; 313 | this.element.setAttribute('aria-pressed', this.active); 314 | } 315 | 316 | onClick(event) { 317 | if (!this.vbgInstance) return; 318 | if (this.active) { 319 | this.vbgInstance.pause(); 320 | } else { 321 | this.vbgInstance.play(); 322 | } 323 | } 324 | } 325 | 326 | export class MuteToggle { 327 | constructor(muteToggleElem, vbgInstance) { 328 | if (!muteToggleElem) return; 329 | 330 | this.element = muteToggleElem; 331 | this.targetSelector = this.element.getAttribute('data-target'); 332 | 333 | if (!this.targetSelector) return; 334 | this.active = false; 335 | 336 | if (this.element.hasAttribute('aria-pressed')) { 337 | this.active = this.element.getAttribute('aria-pressed') === 'true'; 338 | } else { 339 | this.element.setAttribute('aria-pressed', this.active); 340 | } 341 | 342 | this.element.setAttribute('role', 'switch'); 343 | 344 | this.targetElem = document.querySelector(this.targetSelector); 345 | if (!this.targetElem) return; 346 | 347 | if (vbgInstance) this.vbgInstance = vbgInstance; 348 | 349 | this.targetElem.addEventListener('video-background-ready', this.onReady.bind(this)); 350 | this.targetElem.addEventListener('video-background-mute', this.onMute.bind(this)); 351 | this.targetElem.addEventListener('video-background-unmute', this.onUnmute.bind(this)); 352 | this.targetElem.addEventListener('video-background-destroyed', this.onDestroyed.bind(this)); 353 | 354 | this.element.addEventListener('click', this.onClick.bind(this)); 355 | } 356 | 357 | onReady(event) { 358 | this.vbgInstance = event.detail; 359 | if (this.vbgInstance.params.muted) { 360 | this.active = true; 361 | this.element.setAttribute('aria-pressed', this.active); 362 | } 363 | } 364 | 365 | onMute(event) { 366 | if (!this.vbgInstance) this.vbgInstance = event.detail; 367 | this.active = true; 368 | this.element.setAttribute('aria-pressed', this.active); 369 | } 370 | 371 | onUnmute(event) { 372 | if (!this.vbgInstance) this.vbgInstance = event.detail; 373 | this.active = false; 374 | this.element.setAttribute('aria-pressed', this.active); 375 | } 376 | 377 | onDestroyed(event) { 378 | this.vbgInstance = null; 379 | this.active = false; 380 | this.element.setAttribute('aria-pressed', this.active); 381 | } 382 | 383 | onClick(event) { 384 | if (!this.vbgInstance) return; 385 | if (this.active) { 386 | this.vbgInstance.unmute(); 387 | } else { 388 | this.vbgInstance.mute(); 389 | } 390 | } 391 | } 392 | 393 | // TODO: this can be achieved with custom elements... Maybe it's finally time to use them? 394 | class GeneralFactory { 395 | constructor(selector, callback, uidAttribute = 'data-uid') { 396 | this.instances = {}; 397 | this.selector = selector; 398 | this.elements = []; 399 | this.callback = callback; 400 | this.uidAttribute = uidAttribute; 401 | 402 | if (!callback || typeof callback !== 'function') return; 403 | 404 | if (typeof this.selector === 'string') { 405 | this.elements = document.querySelectorAll(this.selector); 406 | } 407 | 408 | if (this.selector instanceof Element) { 409 | this.elements = [this.selector]; 410 | } 411 | 412 | if (this.selector instanceof NodeList) { 413 | this.elements = this.selector; 414 | } 415 | 416 | for (let i = 0; i < this.elements.length; i++) { 417 | this.add(this.elements[i]); 418 | 419 | // TODO: maybe manage elements array? 420 | } 421 | } 422 | 423 | basicUID() { 424 | return Date.now().toString(36) + Math.random().toString(36).substring(2); 425 | } 426 | 427 | generateUID() { 428 | let tempuid = this.basicUID(); 429 | if (!this.instances.hasOwnProperty(tempuid)) return tempuid; 430 | return this.generateUID(); 431 | } 432 | 433 | add(element) { 434 | let id = element.getAttribute('id'); 435 | if (!id || this.instances.hasOwnProperty(id)) { 436 | id = element.getAttribute(this.uidAttribute); 437 | 438 | if (!id || this.instances.hasOwnProperty(id)) { 439 | id = this.generateUID(); 440 | element.setAttribute(this.uidAttribute, id); 441 | } 442 | } 443 | 444 | if (this.callback && typeof this.callback === 'function') 445 | this.instances[id] = this.callback(element, id, this); 446 | } 447 | 448 | getID(element) { 449 | if (!element) return; 450 | if (typeof element === 'string') return element; 451 | const id = element.getAttribute('id'); 452 | if (id && this.instances.hasOwnProperty(id)) return id; 453 | const uid = element.getAttribute(this.uidAttribute); 454 | if (uid && this.instances.hasOwnProperty(uid)) return uid; 455 | } 456 | 457 | get(element) { 458 | if (!element) return; 459 | const id = this.getID(element); 460 | if (!id) return; 461 | return this.instances[id]; 462 | } 463 | 464 | destroy(element) { 465 | if (!element) return; 466 | const id = this.getID(element); 467 | if (!id) return; 468 | const instance = this.instances[id]; 469 | if (instance.hasOwnProperty('destroy') && typeof instance.destroy == 'function') this.instances[id].destroy(); 470 | delete this.instances[id]; 471 | } 472 | 473 | destroyAll() { 474 | for (const uid in this.instances) { 475 | const instance = this.instances[uid]; 476 | if (instance.hasOwnProperty('destroy') && typeof instance.destroy == 'function') instance.destroy(); 477 | delete this.instances[uid]; 478 | } 479 | } 480 | } 481 | 482 | export class VideoBackgroundGroups extends GeneralFactory { 483 | constructor(selector = '.js-vbg-group', videoBackgroundSelector, videoBackgroundFactoryInstance) { 484 | super(selector, (element, id, factoryInstance) => new VideoBackgroundGroup(element, videoBackgroundSelector, videoBackgroundFactoryInstance)); 485 | } 486 | } 487 | -------------------------------------------------------------------------------- /src/lib/super-video-background.js: -------------------------------------------------------------------------------- 1 | import { generateActionButton } from './buttons.js'; 2 | import { isArray, stringToType, isMobile, parseResolutionString, proportionalParentCoverResize, percentage, fixed } from 'book-of-spells'; 3 | 4 | export class SuperVideoBackground { 5 | constructor(elem, params, id, uid, type, factoryInstance) { 6 | if (!id) return; 7 | this.is_mobile = isMobile(); 8 | this.type = type; 9 | this.id = id; 10 | this.factoryInstance = factoryInstance; 11 | 12 | this.element = elem; 13 | this.playerElement = null; 14 | this.uid = uid; 15 | this.element.setAttribute('data-vbg-uid', uid); 16 | 17 | this.buttons = {}; 18 | this.isIntersecting = false; 19 | 20 | this.paused = false; // user requested pause. used for blocking intersection softPlay 21 | this.muted = false; 22 | this.currentState = 'notstarted'; 23 | 24 | this.initialPlay = false; 25 | this.initialVolume = false; 26 | 27 | this.volume = 1; 28 | 29 | this.params = {}; 30 | 31 | const DEFAULTS = { 32 | 'pause': false, //deprecated 33 | 'play-button': false, 34 | 'mute-button': false, 35 | 'autoplay': true, 36 | 'muted': true, 37 | 'loop': true, 38 | 'mobile': true, 39 | 'load-background': false, 40 | 'resolution': '16:9', 41 | 'inline-styles': true, 42 | 'fit-box': false, 43 | 'offset': 100, // since showinfo is deprecated and ignored after September 25, 2018. we add +100 to hide it in the overflow 44 | 'start-at': 0, 45 | 'end-at': 0, 46 | 'poster': null, 47 | 'always-play': false, 48 | 'volume': 1, 49 | 'no-cookie': true, 50 | 'force-on-low-battery': false, 51 | 'lazyloading': false, 52 | 'title': 'Video background' 53 | }; 54 | 55 | this.params = this.parseProperties(params, DEFAULTS, this.element, ['data-ytbg-', 'data-vbg-']); 56 | 57 | //pause deprecated 58 | if (this.params.pause) { 59 | this.params['play-button'] = this.params.pause; 60 | } 61 | 62 | this.params.resolution_mod = parseResolutionString(this.params.resolution); 63 | 64 | this.muted = this.params.muted; 65 | 66 | this.volume = this.params.volume; 67 | 68 | this.currentTime = this.params['start-at'] || 0; 69 | this.duration = this.params['end-at'] || 0; 70 | this.percentComplete = 0; 71 | if (this.params['start-at']) this.percentComplete = this.timeToPercentage(this.params['start-at']); 72 | 73 | this.buildWrapperHTML(); 74 | 75 | if (this.is_mobile && !this.params.mobile) return; 76 | 77 | if (this.params['play-button']) { 78 | generateActionButton(this, { 79 | name: 'playing', 80 | className: 'play-toggle', 81 | innerHtml: '', 82 | initialState: !this.paused, 83 | stateClassName: 'paused', 84 | condition_parameter: 'paused', 85 | stateChildClassNames: ['fa-pause-circle', 'fa-play-circle'], 86 | actions: ['play', 'pause'] 87 | }); 88 | } 89 | 90 | if (this.params['mute-button']) { 91 | generateActionButton(this, { 92 | name: 'muted', 93 | className: 'mute-toggle', 94 | innerHtml: '', 95 | initialState: this.muted, 96 | stateClassName: 'muted', 97 | condition_parameter: 'muted', 98 | stateChildClassNames: ['fa-volume-up', 'fa-volume-mute'], 99 | actions: ['unmute', 'mute'] 100 | }); 101 | } 102 | } 103 | 104 | timeToPercentage(time) { 105 | if (time <= this.params['start-at']) return 0; 106 | if (time >= this.duration) return 100; 107 | if (time <= 0) return 0; 108 | time -= this.params['start-at']; // normalize 109 | const duration = this.duration - this.params['start-at']; // normalize 110 | return percentage(time, duration); 111 | } 112 | 113 | percentageToTime(percentage) { 114 | if (!this.duration) return this.params['start-at'] || 0; 115 | if (percentage > 100) return this.duration; 116 | if (percentage <= 0) return this.params['start-at'] || 0; 117 | const duration = this.duration - this.params['start-at']; // normalize 118 | let time = percentage * duration / 100; 119 | time = fixed(time, 3) 120 | if (time > duration) time = duration; 121 | if (this.params['start-at']) time += this.params['start-at']; // normalize 122 | return time; 123 | } 124 | 125 | resize(element) { 126 | if (!this.params['fit-box']) proportionalParentCoverResize(element || this.playerElement, this.params.resolution_mod, this.params.offset); 127 | this.dispatchEvent('video-background-resize'); 128 | } 129 | 130 | stylePlayerElement(element) { 131 | if (!element) return; 132 | 133 | if (this.params['inline-styles']) { 134 | element.style.top = '50%'; 135 | element.style.left = '50%'; 136 | element.style.transform = 'translateX(-50%) translateY(-50%)'; 137 | element.style.position = 'absolute'; 138 | element.style.opacity = 0; 139 | } 140 | 141 | if (this.params['fit-box']) { 142 | element.style.width = '100%'; 143 | element.style.height = '100%'; 144 | } 145 | } 146 | 147 | buildWrapperHTML() { 148 | const parent = this.element.parentNode; 149 | // wrap 150 | this.element.classList.add('youtube-background', 'video-background'); 151 | 152 | //set css rules 153 | const wrapper_styles = { 154 | "height" : "100%", 155 | "width" : "100%", 156 | "z-index": "0", 157 | "position": "absolute", 158 | "overflow": "hidden", 159 | "top": 0, // added by @insad 160 | "left": 0, 161 | "bottom": 0, 162 | "right": 0 163 | }; 164 | 165 | if (!this.params['mute-button']) { 166 | wrapper_styles["pointer-events"] = "none" // avoid right mouse click popup menu 167 | } 168 | 169 | if (this.params['load-background'] || this.params['poster']) { 170 | this.loadBackground(this.id); 171 | if (this.params['poster']) wrapper_styles['background-image'] = `url(${ this.params['poster'] })`; 172 | wrapper_styles['background-size'] = 'cover'; 173 | wrapper_styles['background-repeat'] = 'no-repeat'; 174 | wrapper_styles['background-position'] = 'center'; 175 | } 176 | 177 | if (this.params['inline-styles']) { 178 | for (let property in wrapper_styles) { 179 | this.element.style[property] = wrapper_styles[property]; 180 | } 181 | 182 | if (!['absolute', 'fixed', 'relative', 'sticky'].indexOf(parent.style.position)) { 183 | parent.style.position = 'relative'; 184 | } 185 | } 186 | 187 | // set play/mute controls wrap 188 | if (this.params['play-button'] || this.params['mute-button']) { 189 | const controls = document.createElement('div'); 190 | controls.className = 'video-background-controls'; 191 | 192 | controls.style.position = 'absolute'; 193 | controls.style.top = '10px'; 194 | controls.style.right = '10px'; 195 | controls.style['z-index'] = 2; 196 | 197 | this.controls_element = controls; 198 | parent.appendChild(controls); 199 | } 200 | 201 | return this.element; 202 | } 203 | 204 | loadBackground(id) { 205 | if (!this.params['load-background']) return; 206 | if (!id) return; 207 | if (this.type === 'youtube') this.element.style['background-image'] = `url(https://img.youtube.com/vi/${id}/hqdefault.jpg)`; 208 | if (this.type === 'vimeo') this.element.style['background-image'] = `url(https://vumbnail.com/${id}.jpg)`; 209 | } 210 | 211 | destroy() { 212 | this.playerElement.remove(); 213 | this.element.classList.remove('youtube-background', 'video-background'); 214 | this.element.removeAttribute('data-vbg-uid'); 215 | this.element.style = ''; 216 | 217 | if (this.params['play-button'] || this.params['mute-button']) { 218 | this.controls_element.remove(); 219 | } 220 | 221 | if (this.timeUpdateTimer) clearInterval(this.timeUpdateTimer); 222 | this.dispatchEvent('video-background-destroyed'); 223 | } 224 | 225 | setDuration(duration) { 226 | if (this.duration === duration) return; 227 | 228 | if (this.params['end-at']) { 229 | if (duration > this.params['end-at']) { 230 | this.duration = this.params['end-at']; 231 | return; 232 | } 233 | if (duration < this.params['end-at']) { 234 | this.duration = duration; 235 | return; 236 | } 237 | } else { 238 | this.duration = duration; 239 | return; 240 | } 241 | 242 | if (duration <= 0) this.duration = this.params['end-at']; 243 | } 244 | 245 | setStartAt(startAt) { 246 | this.params['start-at'] = startAt; 247 | } 248 | 249 | setEndAt(endAt) { 250 | this.params['end-at'] = endAt; 251 | if (this.duration > endAt) this.duration = endAt; 252 | if (this.currentTime > endAt) this.onVideoEnded(); 253 | } 254 | 255 | dispatchEvent(name) { 256 | this.element.dispatchEvent(new CustomEvent(name, { bubbles: true, detail: this })); 257 | } 258 | 259 | shouldPlay() { 260 | if (this.currentState === 'ended' && !this.params.loop) return false; 261 | if (this.params['always-play'] && this.currentState !== 'playing') return true; 262 | if (this.isIntersecting && this.params.autoplay && this.currentState !== 'playing') return true; 263 | return false; 264 | } 265 | 266 | mobileLowBatteryAutoplayHack() { 267 | if (!this.params['force-on-low-battery']) return; 268 | if (!this.is_mobile && this.params.mobile) return; 269 | 270 | const forceAutoplay = function() { 271 | if (!this.initialPlay && this.params.autoplay && this.params.muted) { 272 | this.softPlay(); 273 | 274 | if (!this.isIntersecting && !this.params['always-play']) { 275 | this.softPause(); 276 | } 277 | } 278 | } 279 | 280 | document.addEventListener('touchstart', forceAutoplay.bind(this), { once: true }); 281 | } 282 | 283 | parseProperties(params, defaults, element, attr_prefix) { 284 | let res_params = {}; 285 | 286 | if (!params) { 287 | res_params = defaults; 288 | } else { 289 | for (let k in defaults) { 290 | //load in defaults if the param hasn't been set 291 | res_params[k] = !params.hasOwnProperty(k) ? defaults[k] : params[k]; 292 | } 293 | } 294 | 295 | if (!element) return res_params; 296 | // load params from data attributes 297 | for (let k in res_params) { 298 | let data; 299 | 300 | if (isArray(attr_prefix)) { 301 | for (let i = 0; i < attr_prefix.length; i++) { 302 | const temp_data = element.getAttribute(attr_prefix[i]+k); 303 | if (temp_data) { 304 | data = temp_data; 305 | break; 306 | } 307 | } 308 | } else { 309 | data = element.getAttribute(attr_prefix+k); 310 | } 311 | 312 | if (data !== undefined && data !== null) { 313 | res_params[k] = stringToType(data); 314 | } 315 | } 316 | 317 | return res_params; 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /src/lib/video-background.js: -------------------------------------------------------------------------------- 1 | import { SuperVideoBackground } from './super-video-background.js'; 2 | import { RE_VIDEO } from 'book-of-spells'; 3 | 4 | export class VideoBackground extends SuperVideoBackground { 5 | constructor(elem, params, vid_data, uid, factoryInstance) { 6 | super(elem, params, vid_data.link, uid, 'video', factoryInstance); 7 | if (!vid_data || !vid_data.link) return; 8 | if (this.is_mobile && !this.params.mobile) return; 9 | 10 | this.src = vid_data.link; 11 | this.ext = /(?:\.([^.]+))?$/.exec(vid_data.id)[1]; 12 | this.uid = uid; 13 | this.element.setAttribute('data-vbg-uid', uid); 14 | this.player = null; 15 | this.buttons = {}; 16 | 17 | this.MIME_MAP = { 18 | 'ogv' : 'video/ogg', 19 | 'ogm' : 'video/ogg', 20 | 'ogg' : 'video/ogg', 21 | 'avi' : 'video/avi', 22 | 'mp4' : 'video/mp4', 23 | 'webm' : 'video/webm', 24 | 'm4v' : 'video/x-m4v', 25 | 'mov' : 'video/quicktime', 26 | 'qt' : 'video/quicktime', 27 | }; 28 | 29 | this.mime = this.MIME_MAP[this.ext.toLowerCase()]; 30 | 31 | this.injectPlayer(); 32 | 33 | this.mobileLowBatteryAutoplayHack(); 34 | this.dispatchEvent('video-background-ready'); 35 | } 36 | 37 | generatePlayerElement() { 38 | const playerElement = document.createElement('video'); 39 | if (this.params.title) playerElement.setAttribute('title', this.params.title); 40 | playerElement.setAttribute('playsinline', ''); 41 | if (this.params.loop) playerElement.setAttribute('loop', ''); 42 | if (this.params.autoplay && (this.params['always-play'] || this.isIntersecting)) { 43 | playerElement.setAttribute('autoplay', ''); 44 | playerElement.autoplay = true; 45 | } 46 | if (this.muted) { 47 | playerElement.setAttribute('muted', ''); 48 | playerElement.muted = true; 49 | } 50 | if (this.params['lazyloading']) playerElement.setAttribute('loading', 'lazy'); 51 | 52 | return playerElement; 53 | } 54 | 55 | injectPlayer() { 56 | this.player = this.generatePlayerElement(); 57 | this.playerElement = this.player; 58 | 59 | if (this.volume !== 1 && !this.muted) this.setVolume(this.volume); 60 | 61 | this.playerElement.setAttribute('id', this.uid) 62 | 63 | this.stylePlayerElement(this.playerElement); 64 | 65 | this.player.addEventListener('loadedmetadata', this.onVideoLoadedMetadata.bind(this)); 66 | this.player.addEventListener('durationchange', this.onVideoLoadedMetadata.bind(this)); 67 | this.player.addEventListener('canplay', this.onVideoCanPlay.bind(this)); 68 | this.player.addEventListener('timeupdate', this.onVideoTimeUpdate.bind(this)); 69 | this.player.addEventListener('play', this.onVideoPlay.bind(this)); 70 | this.player.addEventListener('pause', this.onVideoPause.bind(this)); 71 | this.player.addEventListener('waiting', this.onVideoBuffering.bind(this)); 72 | this.player.addEventListener('ended', this.onVideoEnded.bind(this)); 73 | 74 | this.element.appendChild(this.playerElement); 75 | const source = document.createElement('source'); 76 | source.setAttribute('src', this.src); 77 | source.setAttribute('type', this.mime); 78 | this.playerElement.appendChild(source); 79 | this.resize(this.playerElement); 80 | } 81 | 82 | updateState(state) { 83 | this.currentState = state; 84 | this.dispatchEvent('video-background-state-change'); 85 | } 86 | 87 | /* ===== API ===== */ 88 | 89 | setSource(url) { 90 | const pts = url.match(RE_VIDEO); 91 | if (!pts || !pts.length) return; 92 | this.id = pts[1]; 93 | this.ext = /(?:\.([^.]+))?$/.exec(this.id)[1]; 94 | this.mime = this.MIME_MAP[this.ext.toLowerCase()]; 95 | this.playerElement.innerHTML = ''; 96 | const source = document.createElement('source'); 97 | source.setAttribute('src', url); 98 | source.setAttribute('type', this.mime); 99 | this.playerElement.appendChild(source); 100 | this.src = url; 101 | 102 | if (this.element.hasAttribute('data-vbg')) this.element.setAttribute('data-vbg', this.src); 103 | if (this.element.hasAttribute('data-ytbg')) this.element.setAttribute('data-ytbg', this.src); 104 | } 105 | 106 | onVideoLoadedMetadata() { 107 | this.setDuration(this.player.duration); 108 | } 109 | 110 | onVideoCanPlay() { 111 | this.setDuration(this.player.duration); 112 | } 113 | 114 | onVideoTimeUpdate() { 115 | this.currentTime = this.player.currentTime; 116 | this.percentComplete = this.timeToPercentage(this.player.currentTime); 117 | this.dispatchEvent('video-background-time-update'); 118 | 119 | if (this.params['end-at'] && this.currentTime >= this.duration) { 120 | this.onVideoEnded(); 121 | } 122 | } 123 | 124 | onVideoPlay() { 125 | if (!this.initialPlay) { 126 | this.initialPlay = true; 127 | this.playerElement.style.opacity = 1; 128 | } 129 | 130 | const seconds = this.player.currentTime; 131 | if (this.params['start-at'] && seconds <= this.params['start-at']) { 132 | this.seekTo(this.params['start-at']); 133 | } 134 | 135 | if (this.duration && seconds >= this.duration) { 136 | this.seekTo(this.params['start-at']); 137 | } 138 | 139 | this.updateState('playing'); 140 | this.dispatchEvent('video-background-play'); 141 | } 142 | 143 | onVideoPause() { 144 | this.updateState('paused'); 145 | this.dispatchEvent('video-background-pause'); 146 | } 147 | 148 | onVideoEnded() { 149 | this.updateState('ended'); 150 | this.dispatchEvent('video-background-ended'); 151 | if (!this.params.loop) return this.pause(); 152 | 153 | this.seekTo(this.params['start-at']); 154 | this.onVideoPlay(); 155 | } 156 | 157 | onVideoBuffering() { 158 | this.updateState('buffering'); 159 | } 160 | 161 | seek(percentage) { 162 | this.seekTo(this.percentageToTime(percentage)); 163 | } 164 | 165 | seekTo(seconds) { 166 | if (!this.player) return; 167 | if (this.player.hasOwnProperty('fastSeek')) { 168 | this.player.fastSeek(seconds); 169 | return; 170 | } 171 | this.player.currentTime = seconds; 172 | this.dispatchEvent('video-background-seeked'); 173 | } 174 | 175 | softPause() { 176 | if (!this.player || this.currentState === 'paused') return; 177 | this.player.pause(); 178 | } 179 | 180 | softPlay() { 181 | if (!this.player || this.currentState === 'playing') return; 182 | this.player.play(); 183 | } 184 | 185 | play() { 186 | if (!this.player) return; 187 | this.paused = false; 188 | 189 | this.player.play(); 190 | } 191 | 192 | pause() { 193 | if (!this.player) return; 194 | this.paused = true; 195 | 196 | this.player.pause(); 197 | } 198 | 199 | unmute() { 200 | if (!this.player) return; 201 | this.muted = false; 202 | 203 | this.player.muted = false; 204 | if (!this.initialVolume) { 205 | this.initialVolume = true; 206 | this.setVolume(this.params.volume); 207 | } 208 | this.dispatchEvent('video-background-unmute'); 209 | } 210 | 211 | mute() { 212 | if (!this.player) return; 213 | this.muted = true; 214 | 215 | this.player.muted = true; 216 | this.dispatchEvent('video-background-mute'); 217 | } 218 | 219 | getVolume() { 220 | if (!this.player) return; 221 | return this.player.volume; 222 | } 223 | 224 | setVolume(volume) { 225 | if (!this.player) return; 226 | this.volume = volume; 227 | 228 | this.player.volume = volume; 229 | this.dispatchEvent('video-background-volume-change'); 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/lib/vimeo-background.js: -------------------------------------------------------------------------------- 1 | import { SuperVideoBackground } from './super-video-background.js'; 2 | import { RE_VIMEO } from 'book-of-spells'; 3 | 4 | export class VimeoBackground extends SuperVideoBackground { 5 | constructor(elem, params, id, uid, factoryInstance) { 6 | super(elem, params, id.id, uid, 'vimeo', factoryInstance); 7 | if (!id) return; 8 | this.unlisted = id.unlisted; 9 | 10 | if (this.is_mobile && !this.params.mobile) return; 11 | this.injectScript(); 12 | 13 | this.player = null; 14 | 15 | this.injectPlayer(); 16 | 17 | this.initVimeoPlayer(); 18 | } 19 | 20 | injectScript() { 21 | const src = 'https://player.vimeo.com/api/player.js'; 22 | if (window.hasOwnProperty('Vimeo') || document.querySelector(`script[src="${src}"]`)) return; 23 | const tag = document.createElement('script'); 24 | tag.async = true; 25 | if (window.hasOwnProperty('onVimeoIframeAPIReady') && typeof window.onVimeoIframeAPIReady === 'function') tag.addEventListener('load', () => { 26 | window.onVimeoIframeAPIReady(); 27 | }); 28 | tag.src = src; 29 | const firstScriptTag = document.getElementsByTagName('script')[0]; 30 | firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); 31 | } 32 | 33 | initVimeoPlayer() { 34 | if (!window.hasOwnProperty('Vimeo') || this.player !== null) return; 35 | this.player = new Vimeo.Player(this.playerElement); 36 | 37 | this.player.on('loaded', this.onVideoPlayerReady.bind(this)); 38 | this.player.on('ended', this.onVideoEnded.bind(this)); 39 | this.player.on('play', this.onVideoPlay.bind(this)); 40 | this.player.on('pause', this.onVideoPause.bind(this)); 41 | this.player.on('bufferstart', this.onVideoBuffering.bind(this)); 42 | this.player.on('timeupdate', this.onVideoTimeUpdate.bind(this)); 43 | // this.player.on('error', this.onVideoError.bind(this)); 44 | 45 | if (this.volume !== 1 && !this.muted) this.setVolume(this.volume); 46 | } 47 | 48 | onVideoError(event) { 49 | console.error(event); 50 | } 51 | 52 | generatePlayerElement() { 53 | const playerElement = document.createElement('iframe'); 54 | if (this.params.title) playerElement.setAttribute('title', this.params.title); 55 | playerElement.setAttribute('frameborder', 0); 56 | playerElement.setAttribute('allow', 'autoplay; mute'); 57 | if (this.params['lazyloading']) playerElement.setAttribute('loading', 'lazy'); 58 | 59 | return playerElement; 60 | } 61 | 62 | generateSrcURL(id, unlisted) { 63 | unlisted = unlisted ? `h=${unlisted}&` : '' 64 | let src = `https://player.vimeo.com/video/${id}?${unlisted}background=1&controls=0`; 65 | 66 | if (this.params.muted) { 67 | src += '&muted=1'; 68 | } 69 | 70 | if (this.params.autoplay && (this.params['always-play'] || this.isIntersecting)) { 71 | src += '&autoplay=1'; 72 | } 73 | 74 | if (this.params.loop) { 75 | src += '&loop=1&autopause=0'; 76 | } 77 | 78 | if (this.params['no-cookie']) { 79 | src += '&dnt=1'; 80 | } 81 | 82 | //WARN❗️: this is a hash not a query param 83 | if (this.params['start-at']) { 84 | src += '#t=' + this.params['start-at'] + 's'; 85 | } 86 | 87 | return src; 88 | } 89 | 90 | injectPlayer() { 91 | this.playerElement = this.generatePlayerElement(); 92 | this.src = this.generateSrcURL(this.id, this.unlisted); 93 | this.playerElement.src = this.src; 94 | this.playerElement.id = this.uid; 95 | 96 | this.stylePlayerElement(this.playerElement); 97 | this.element.appendChild(this.playerElement); 98 | this.resize(this.playerElement); 99 | } 100 | 101 | updateState(state) { 102 | this.currentState = state; 103 | this.dispatchEvent('video-background-state-change'); 104 | } 105 | 106 | /* ===== API ===== */ 107 | 108 | setSource(url) { 109 | const pts = url.match(RE_VIMEO); 110 | if (!pts || !pts.length) return; 111 | 112 | this.id = pts[1]; 113 | this.src = this.generateSrcURL(this.id); 114 | this.playerElement.src = this.src; 115 | 116 | if (this.element.hasAttribute('data-vbg')) this.element.setAttribute('data-vbg', this.src); 117 | if (this.element.hasAttribute('data-ytbg')) this.element.setAttribute('data-ytbg', this.src); 118 | this.loadBackground(this.id); 119 | } 120 | 121 | onVideoPlayerReady() { 122 | this.mobileLowBatteryAutoplayHack(); 123 | 124 | if (this.params['start-at']) this.seekTo(this.params['start-at']); 125 | 126 | if (this.params.autoplay && (this.params['always-play'] || this.isIntersecting)) { 127 | this.player.play(); 128 | } 129 | 130 | this.player.getDuration().then((duration) => { 131 | this.setDuration(duration); 132 | }); 133 | 134 | this.dispatchEvent('video-background-ready'); 135 | } 136 | 137 | onVideoEnded() { 138 | this.updateState('ended'); 139 | this.dispatchEvent('video-background-ended'); 140 | if (!this.params.loop) return this.pause(); 141 | 142 | this.seekTo(this.params['start-at']); 143 | this.updateState('playing'); 144 | this.dispatchEvent('video-background-play'); 145 | } 146 | 147 | onVideoTimeUpdate(event) { 148 | this.currentTime = event.seconds; 149 | this.percentComplete = this.timeToPercentage(event.seconds); 150 | this.dispatchEvent('video-background-time-update'); 151 | this.setDuration(event.duration); 152 | 153 | if (this.params['end-at'] && this.duration && event.seconds >= this.duration) { 154 | this.onVideoEnded(); 155 | } 156 | } 157 | 158 | onVideoBuffering() { 159 | this.updateState('buffering'); 160 | } 161 | 162 | onVideoPlay(event) { 163 | this.setDuration(event.duration); 164 | 165 | if (!this.initialPlay) { 166 | this.initialPlay = true; 167 | this.playerElement.style.opacity = 1; 168 | 169 | // gotta set loop manually, cause for some reason it's true by default 170 | this.player.setLoop(this.params.loop); 171 | 172 | //Hotfixing an issue that it automatically starts playing after buffering on the first load, sometimes, not always, for an unknown reason 173 | if (!(this.params.autoplay && (this.params['always-play'] || this.isIntersecting))) { 174 | return this.player.pause(); 175 | } 176 | } 177 | 178 | const seconds = event.seconds; 179 | if (this.params['start-at'] && seconds < this.params['start-at']) { 180 | this.seekTo(this.params['start-at']); 181 | } 182 | 183 | if (this.duration && seconds >= this.duration) { 184 | this.seekTo(this.params['start-at']); 185 | } 186 | 187 | this.updateState('playing'); 188 | this.dispatchEvent('video-background-play'); 189 | } 190 | 191 | onVideoPause() { 192 | this.updateState('paused'); 193 | this.dispatchEvent('video-background-pause'); 194 | } 195 | 196 | seek(percentage) { 197 | this.seekTo(this.percentageToTime(percentage)); 198 | } 199 | 200 | seekTo(time) { 201 | if (!this.player) return; 202 | this.player.setCurrentTime(time); 203 | this.dispatchEvent('video-background-seeked'); 204 | } 205 | 206 | softPause() { 207 | if (!this.player || this.currentState === 'paused') return; 208 | this.player.pause(); 209 | } 210 | 211 | softPlay() { 212 | if (!this.player || this.currentState === 'playing') return; 213 | this.player.play(); 214 | } 215 | 216 | play() { 217 | if (!this.player) return; 218 | this.paused = false; 219 | 220 | this.player.play(); 221 | } 222 | 223 | pause() { 224 | if (!this.player) return; 225 | this.paused = true; 226 | 227 | this.player.pause(); 228 | } 229 | 230 | unmute() { 231 | if (!this.player) return; 232 | this.muted = false; 233 | 234 | if (!this.initialVolume) { 235 | this.initialVolume = true; 236 | this.setVolume(this.params.volume); 237 | } 238 | this.player.setMuted(false); 239 | this.dispatchEvent('video-background-unmute'); 240 | } 241 | 242 | mute() { 243 | if (!this.player) return; 244 | this.muted = true; 245 | 246 | this.player.setMuted(true); 247 | this.dispatchEvent('video-background-mute'); 248 | } 249 | 250 | getVolume() { 251 | if (!this.player) return; 252 | return this.player.getVolume(); 253 | } 254 | 255 | setVolume(volume) { 256 | if (!this.player) return; 257 | this.volume = volume; 258 | 259 | this.player.setVolume(volume); 260 | this.dispatchEvent('video-background-volume-change'); 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /src/lib/youtube-background.js: -------------------------------------------------------------------------------- 1 | import { SuperVideoBackground } from './super-video-background.js'; 2 | import { RE_YOUTUBE } from 'book-of-spells'; 3 | 4 | export class YoutubeBackground extends SuperVideoBackground { 5 | constructor(elem, params, id, uid, factoryInstance) { 6 | super(elem, params, id, uid, 'youtube', factoryInstance); 7 | 8 | if (!id) return; 9 | if (this.is_mobile && !this.params.mobile) return; 10 | this.injectScript(); 11 | 12 | this.player = null; 13 | 14 | this.injectPlayer(); 15 | 16 | this.STATES = { 17 | '-1': 'notstarted', 18 | '0': 'ended', 19 | '1': 'playing', 20 | '2': 'paused', 21 | '3': 'buffering', 22 | '5': 'cued' 23 | }; 24 | 25 | this.timeUpdateTimer = null; 26 | this.timeUpdateInterval = 250; 27 | 28 | this.initYTPlayer(); 29 | } 30 | 31 | startTimeUpdateTimer() { 32 | if (this.timeUpdateTimer) return; 33 | this.timeUpdateTimer = setInterval(this.onVideoTimeUpdate.bind(this), this.timeUpdateInterval); 34 | }; 35 | 36 | stopTimeUpdateTimer() { 37 | clearInterval(this.timeUpdateTimer); 38 | this.timeUpdateTimer = null; 39 | }; 40 | 41 | convertState(state) { 42 | return this.STATES[state]; 43 | } 44 | 45 | initYTPlayer() { 46 | if (!window.hasOwnProperty('YT') || this.player !== null) return; 47 | 48 | this.player = new YT.Player(this.uid, { 49 | events: { 50 | 'onReady': this.onVideoPlayerReady.bind(this), 51 | 'onStateChange': this.onVideoStateChange.bind(this), 52 | // 'onError': this.onVideoError.bind(this) 53 | } 54 | }); 55 | 56 | if (this.volume !== 1 && !this.muted) this.setVolume(this.volume); 57 | } 58 | 59 | onVideoError(event) { 60 | console.error(event); 61 | } 62 | 63 | injectScript() { 64 | const src = 'https://www.youtube.com/player_api'; 65 | if (window.hasOwnProperty('YT') || document.querySelector(`script[src="${src}"]`)) return 66 | const tag = document.createElement('script'); 67 | tag.async = true; 68 | tag.src = src; 69 | const firstScriptTag = document.getElementsByTagName('script')[0]; 70 | firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); 71 | } 72 | 73 | generatePlayerElement() { 74 | const playerElement = document.createElement('iframe'); 75 | if (this.params.title) playerElement.setAttribute('title', this.params.title); 76 | playerElement.setAttribute('frameborder', 0); 77 | playerElement.setAttribute('allow', 'autoplay; mute'); 78 | if (this.params['lazyloading']) playerElement.setAttribute('loading', 'lazy'); 79 | 80 | return playerElement; 81 | } 82 | 83 | generateSrcURL(id) { 84 | let site = 'https://www.youtube.com/embed/'; 85 | if (this.params['no-cookie']) { 86 | site = 'https://www.youtube-nocookie.com/embed/'; 87 | } 88 | let src = `${site}${id}?&enablejsapi=1&disablekb=1&controls=0&rel=0&iv_load_policy=3&cc_load_policy=0&playsinline=1&showinfo=0&modestbranding=1&fs=0`; 89 | 90 | if (this.params.muted) { 91 | src += '&mute=1'; 92 | } 93 | 94 | if (this.params.autoplay && (this.params['always-play'] || this.isIntersecting)) { 95 | src += '&autoplay=1'; 96 | } 97 | 98 | if (this.params.loop) { 99 | src += '&loop=1'; 100 | } 101 | 102 | return src; 103 | } 104 | 105 | injectPlayer() { 106 | this.playerElement = this.generatePlayerElement(); 107 | this.src = this.generateSrcURL(this.id); 108 | this.playerElement.src = this.src; 109 | this.playerElement.id = this.uid; 110 | 111 | this.stylePlayerElement(this.playerElement); 112 | this.element.appendChild(this.playerElement); 113 | this.resize(this.playerElement); 114 | } 115 | 116 | /* ===== API ===== */ 117 | 118 | setSource(url) { 119 | const pts = url.match(RE_YOUTUBE); 120 | if (!pts || !pts.length) return; 121 | 122 | this.id = pts[1]; 123 | this.src = this.generateSrcURL(this.id); 124 | this.playerElement.src = this.src; 125 | 126 | if (this.element.hasAttribute('data-vbg')) this.element.setAttribute('data-vbg', this.src); 127 | if (this.element.hasAttribute('data-ytbg')) this.element.setAttribute('data-ytbg', this.src); 128 | this.loadBackground(this.id); 129 | } 130 | 131 | onVideoTimeUpdate() { 132 | const ctime = this.player.getCurrentTime(); 133 | if (ctime === this.currentTime) return; 134 | this.currentTime = ctime; 135 | this.percentComplete = this.timeToPercentage(this.currentTime); 136 | if (this.params['end-at'] && this.duration && this.currentTime >= this.duration) { 137 | this.currentState = 'ended'; 138 | this.dispatchEvent('video-background-state-change'); 139 | this.onVideoEnded(); 140 | this.stopTimeUpdateTimer(); 141 | return; 142 | } 143 | this.dispatchEvent('video-background-time-update'); 144 | } 145 | 146 | onVideoPlayerReady() { 147 | this.mobileLowBatteryAutoplayHack(); 148 | 149 | if (this.params.autoplay && (this.params['always-play'] || this.isIntersecting)) { 150 | if (this.params['start-at']) this.seekTo(this.params['start-at']); 151 | this.player.playVideo(); 152 | } 153 | 154 | this.setDuration(this.player.getDuration()); 155 | 156 | this.dispatchEvent('video-background-ready'); 157 | } 158 | 159 | onVideoStateChange(event) { 160 | this.currentState = this.convertState(event.data); 161 | 162 | if (this.currentState === 'ended') this.onVideoEnded(); 163 | 164 | if (this.currentState === 'notstarted' && this.params.autoplay) { 165 | this.seekTo(this.params['start-at']); 166 | this.player.playVideo(); 167 | } 168 | 169 | if (this.currentState === 'playing') this.onVideoPlay(); 170 | 171 | if (this.currentState === 'paused') this.onVideoPause(); 172 | 173 | this.dispatchEvent('video-background-state-change'); 174 | } 175 | 176 | onVideoPlay() { 177 | if (!this.initialPlay) { 178 | this.initialPlay = true; 179 | this.playerElement.style.opacity = 1; 180 | } 181 | 182 | const seconds = this.player.getCurrentTime(); 183 | if (this.params['start-at'] && seconds < this.params['start-at'] ) { 184 | this.seekTo(this.params['start-at']); 185 | } 186 | 187 | if (this.duration && seconds >= this.duration) { 188 | this.seekTo(this.params['start-at']); 189 | } 190 | 191 | if (!this.duration) { 192 | this.setDuration(this.player.getDuration()); 193 | } 194 | 195 | this.dispatchEvent('video-background-play'); 196 | this.startTimeUpdateTimer(); 197 | } 198 | 199 | onVideoPause() { 200 | this.stopTimeUpdateTimer(); 201 | this.dispatchEvent('video-background-pause'); 202 | } 203 | 204 | onVideoEnded() { 205 | this.dispatchEvent('video-background-ended'); 206 | 207 | if (!this.params.loop) return this.pause(); 208 | this.seekTo(this.params['start-at']); 209 | this.player.playVideo(); 210 | } 211 | 212 | seek(percentage) { 213 | this.seekTo(this.percentageToTime(percentage), true); 214 | } 215 | 216 | seekTo(seconds, allowSeekAhead = true) { 217 | if (!this.player) return; 218 | this.player.seekTo(seconds, allowSeekAhead); 219 | this.dispatchEvent('video-background-seeked'); 220 | } 221 | 222 | softPause() { 223 | if (!this.player || this.currentState === 'paused') return; 224 | this.stopTimeUpdateTimer(); 225 | this.player.pauseVideo(); 226 | } 227 | 228 | softPlay() { 229 | if (!this.player || this.currentState === 'playing') return; 230 | this.player.playVideo(); 231 | } 232 | 233 | play() { 234 | if (!this.player) return; 235 | this.paused = false; 236 | 237 | this.player.playVideo(); 238 | } 239 | 240 | pause() { 241 | if (!this.player) return; 242 | this.paused = true; 243 | this.stopTimeUpdateTimer(); 244 | this.player.pauseVideo(); 245 | } 246 | 247 | unmute() { 248 | if (!this.player) return; 249 | this.muted = false; 250 | 251 | if (!this.initialVolume) { 252 | this.initialVolume = true; 253 | this.setVolume(this.params.volume); 254 | } 255 | this.player.unMute(); 256 | this.dispatchEvent('video-background-unmute'); 257 | } 258 | 259 | mute() { 260 | if (!this.player) return; 261 | this.muted = true; 262 | 263 | this.player.mute(); 264 | this.dispatchEvent('video-background-mute'); 265 | } 266 | 267 | getVolume() { 268 | if (!this.player) return; 269 | return this.player.getVolume() / 100; 270 | } 271 | 272 | setVolume(volume) { 273 | if (!this.player) return; 274 | this.volume = volume; 275 | 276 | this.player.setVolume(volume * 100); 277 | this.dispatchEvent('video-background-volume-change'); 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { VideoBackgrounds } from './video-backgrounds.js'; 2 | 3 | if (typeof jQuery == 'function') { 4 | (function ($) { 5 | $.fn.youtube_background = function (params) { 6 | const $this = $(this); 7 | if (window.hasOwnProperty('VIDEO_BACKGROUNDS')) { 8 | window.VIDEO_BACKGROUNDS.add($this, params); 9 | return $this; 10 | } 11 | window.VIDEO_BACKGROUNDS = new VideoBackgrounds(this, params); 12 | return $this; 13 | }; 14 | })(jQuery); 15 | } 16 | 17 | window.VideoBackgrounds = VideoBackgrounds; 18 | -------------------------------------------------------------------------------- /src/video-backgrounds.js: -------------------------------------------------------------------------------- 1 | import { YoutubeBackground } from './lib/youtube-background.js'; 2 | import { VimeoBackground } from './lib/vimeo-background.js'; 3 | import { VideoBackground } from './lib/video-background.js'; 4 | 5 | import { randomIntInclusive, RE_VIMEO, RE_YOUTUBE, RE_VIDEO } from 'book-of-spells'; 6 | 7 | export class VideoBackgrounds { 8 | constructor(selector, params) { 9 | this.elements = selector; 10 | if (this.elements instanceof Element) this.elements = [this.elements]; 11 | if (typeof this.elements === 'string') this.elements = document.querySelectorAll(selector); 12 | 13 | this.index = {}; 14 | 15 | const self = this; 16 | 17 | this.intersectionObserver = null; 18 | 19 | if ('IntersectionObserver' in window) { 20 | this.intersectionObserver = new IntersectionObserver(function (entries) { 21 | entries.forEach(function (entry) { 22 | const uid = entry.target.getAttribute('data-vbg-uid'); 23 | 24 | if (uid && self.index.hasOwnProperty(uid) && entry.isIntersecting) { 25 | self.index[uid].isIntersecting = true; 26 | try { 27 | if (self.index[uid].player && !self.index[uid].paused) self.index[uid].softPlay(); 28 | } catch (e) { 29 | // console.log(e); 30 | } 31 | } else { 32 | self.index[uid].isIntersecting = false; 33 | try { 34 | if (self.index[uid].player) self.index[uid].softPause(); 35 | } catch (e) { 36 | // console.log(e); 37 | } 38 | } 39 | }); 40 | }); 41 | } 42 | 43 | this.resizeObserver = null; 44 | 45 | if ('ResizeObserver' in window) { 46 | this.resizeObserver = new ResizeObserver(function (entries) { 47 | entries.forEach(function (entry) { 48 | const uid = entry.target.getAttribute('data-vbg-uid'); 49 | 50 | if (uid && self.index.hasOwnProperty(uid)) { 51 | window.requestAnimationFrame(() => self.index[uid].resize()); 52 | } 53 | }); 54 | }); 55 | } else { 56 | window.addEventListener('resize', function () { 57 | for (let k in self.index) { 58 | window.requestAnimationFrame(() => self.index[k].resize(self.index[k].playerElement)); 59 | } 60 | }); 61 | } 62 | 63 | this.initPlayers(); 64 | 65 | if (!this.elements || !this.elements.length) return; 66 | for (let i = 0; i < this.elements.length; i++) { 67 | const element = this.elements[i]; 68 | this.add(element, params); 69 | } 70 | 71 | document.addEventListener('visibilitychange', this.onVisibilityChange.bind(this)); 72 | } 73 | 74 | onVisibilityChange() { 75 | if (document.hidden) return; 76 | 77 | for (let k in this.index) { 78 | const instance = this.index[k]; 79 | if (instance.shouldPlay()) { 80 | instance.softPlay(); 81 | } 82 | } 83 | } 84 | 85 | add(element, params) { 86 | if (!element) return; 87 | if (element.hasAttribute('data-vbg-uid')) return; 88 | 89 | if (!this.intersectionObserver) { 90 | if (!params) params = {}; 91 | params['always-play'] = true; 92 | } 93 | 94 | const link = element.getAttribute('data-youtube') || element.getAttribute('data-vbg'); 95 | const vid_data = this.getVidID(link); 96 | 97 | if (!vid_data) return; 98 | 99 | const uid = this.generateUID(vid_data.id); 100 | 101 | if (!uid) return; 102 | 103 | switch (vid_data.type) { 104 | case 'YOUTUBE': 105 | const yb = new YoutubeBackground(element, params, vid_data.id, uid, this); 106 | this.index[uid] = yb; 107 | break; 108 | case 'VIMEO': 109 | const vm = new VimeoBackground(element, params, vid_data, uid, this); 110 | this.index[uid] = vm; 111 | break; 112 | case 'VIDEO': 113 | const vid = new VideoBackground(element, params, vid_data, uid, this); 114 | this.index[uid] = vid; 115 | break; 116 | } 117 | 118 | if (this.resizeObserver) { 119 | this.resizeObserver.observe(element); 120 | } 121 | 122 | if (!this.index[uid].params['always-play'] && this.intersectionObserver) { 123 | this.intersectionObserver.observe(element); 124 | } 125 | } 126 | 127 | destroy(element) { 128 | const uid = element.uid || element.getAttribute('data-vbg-uid'); 129 | if (uid && this.index.hasOwnProperty(uid)) { 130 | if (!this.index[uid].params['always-play'] && this.intersectionObserver) this.intersectionObserver.unobserve(element); 131 | if (this.resizeObserver) this.resizeObserver.unobserve(element); 132 | this.index[uid].destroy(); 133 | delete this.index[uid]; 134 | } 135 | } 136 | 137 | destroyAll() { 138 | for (let k in this.index) { 139 | this.destroy(this.index[k].playerElement); 140 | } 141 | } 142 | 143 | getVidID(link) { 144 | if (link === undefined && link === null) return; 145 | 146 | this.re = {}; 147 | this.re.YOUTUBE = RE_YOUTUBE; 148 | this.re.VIMEO = RE_VIMEO; 149 | this.re.VIDEO = RE_VIDEO; 150 | 151 | for (let k in this.re) { 152 | const pts = link.match(this.re[k]); 153 | 154 | if (pts && pts.length) { 155 | this.re[k].lastIndex = 0; 156 | const data = { 157 | id: pts[1], 158 | type: k, 159 | regex_pts: pts, 160 | link: link 161 | }; 162 | 163 | if (k === 'VIMEO') { 164 | const unlistedQueryRegex = /(\?|&)h=([^=&#?]+)/; 165 | const unlistedPathRegex = /\/[^\/\:\.]+(\:|\/)([^:?\/]+)\s?$/; 166 | const unlistedQuery = link.match(unlistedPathRegex) || link.match(unlistedQueryRegex); 167 | if (unlistedQuery) data.unlisted = unlistedQuery[2]; 168 | } 169 | 170 | return data; 171 | } 172 | } 173 | 174 | return; 175 | } 176 | 177 | generateUID(pref) { 178 | //index the instance 179 | pref = pref.replace(/[^a-zA-Z0-9\-_]/g, '-'); //sanitize id 180 | pref = pref.replace(/-{2,}/g, '-'); //remove double dashes 181 | pref = pref.replace(/^-+/, '').replace(/-+$/, ''); //trim dashes 182 | pref = 'vbg-'+ pref; //prefix id with 'vbg- 183 | 184 | let uid = pref +'-'+ randomIntInclusive(0, 9999); 185 | while (this.index.hasOwnProperty(uid)) { 186 | uid = pref +'-'+ randomIntInclusive(0, 9999); 187 | } 188 | 189 | return uid; 190 | } 191 | 192 | get(element) { 193 | const uid = typeof element === 'string' ? element : element.getAttribute('data-vbg-uid'); 194 | if (uid && this.index.hasOwnProperty(uid)) return this.index[uid]; 195 | } 196 | 197 | pauseAll() { 198 | for (let k in this.index) { 199 | this.index[k].pause(); 200 | } 201 | } 202 | 203 | playAll() { 204 | for (let k in this.index) { 205 | this.index[k].play(); 206 | } 207 | } 208 | 209 | muteAll() { 210 | for (let k in this.index) { 211 | this.index[k].mute(); 212 | } 213 | } 214 | 215 | unmuteAll() { 216 | for (let k in this.index) { 217 | this.index[k].unmute(); 218 | } 219 | } 220 | 221 | setVolumeAll(volume) { 222 | for (let k in this.index) { 223 | this.index[k].setVolume(volume); 224 | } 225 | } 226 | 227 | initPlayers(callback) { 228 | const self = this; 229 | 230 | window.onYouTubeIframeAPIReady = function () { 231 | for (let k in self.index) { 232 | if (self.index[k] instanceof YoutubeBackground) { 233 | self.index[k].initYTPlayer(); 234 | } 235 | } 236 | 237 | if (callback) { 238 | setTimeout(callback, 100); 239 | } 240 | }; 241 | 242 | if (window.hasOwnProperty('YT') && window.YT.loaded) { 243 | window.onYouTubeIframeAPIReady(); 244 | } 245 | 246 | window.onVimeoIframeAPIReady = function () { 247 | for (let k in self.index) { 248 | if (self.index[k] instanceof VimeoBackground) { 249 | self.index[k].initVimeoPlayer(); 250 | } 251 | } 252 | 253 | if (callback) { 254 | setTimeout(callback, 100); 255 | } 256 | } 257 | 258 | if (window.hasOwnProperty('Vimeo') && window.Vimeo.hasOwnProperty('Player')) { 259 | window.onVimeoIframeAPIReady(); 260 | } 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | font-family: 'Lato', sans-serif; 3 | color: #1d1d1d; 4 | padding: 0px; 5 | margin: 0px; 6 | } 7 | 8 | .example-marquee { 9 | position: relative; 10 | } 11 | 12 | .content { 13 | display: table; 14 | width: 100%; 15 | min-height: 100vh; 16 | z-index: 1; 17 | position: relative; 18 | } 19 | 20 | .content .inner { 21 | display: table-cell; 22 | vertical-align: middle; 23 | text-align: center; 24 | padding-left: 16px; 25 | padding-right: 16px; 26 | } 27 | 28 | .content .inner h1, 29 | .content .inner h2 { 30 | color: white; 31 | text-shadow: 0px 1px 3px rgba(0,0,0,0.5); 32 | } 33 | 34 | .content .inner h1 { 35 | font-size: 62px; 36 | } 37 | 38 | .video-background-controls button { 39 | font-size: 32px; 40 | display: inline-block; 41 | padding: 0px; 42 | margin: 0px; 43 | height: 32px; 44 | width: 32px; 45 | border-radius: 16px; 46 | line-height: 32px; 47 | border: none; 48 | background: none; 49 | appearance: none; 50 | color: white; 51 | filter: drop-shadow(0px 0px 1px black); 52 | cursor: pointer; 53 | opacity: 1; 54 | transition: all 250ms ease-in-out; 55 | margin-left: 10px; 56 | } 57 | 58 | .video-background-controls button:hover { 59 | opacity: 0.5; 60 | } 61 | 62 | :root { 63 | --seek-bar-thumb-color: #fff; 64 | --seek-bar-progress-background: #fff; 65 | --seek-bar-background: rgba(255, 255, 255, 0.4); 66 | } 67 | 68 | .seek-bar-wrapper { 69 | height: 6px; 70 | background: var(--seek-bar-background); 71 | position: relative; 72 | } 73 | 74 | .seek-bar-progress { 75 | -webkit-appearance: none; 76 | -moz-appearance: none; 77 | appearance: none; 78 | overflow: hidden; 79 | background: transparent !important; 80 | border: 0; 81 | position: absolute; 82 | width: 100%; 83 | height: 100%; 84 | top: 0; 85 | left: 0; 86 | z-index: 1; 87 | } 88 | 89 | .seek-bar-progress::-webkit-progress-bar { 90 | background: transparent; 91 | } 92 | 93 | .seek-bar-progress::-webkit-progress-value { 94 | background: var(--seek-bar-progress-background); 95 | } 96 | 97 | .seek-bar-progress::-moz-progress-bar { 98 | background: var(--seek-bar-progress-background); 99 | } 100 | 101 | .seek-bar { 102 | -webkit-appearance: none; 103 | -moz-appearance: none; 104 | appearance: none; 105 | position: relative; 106 | top: calc(50% - 10px); 107 | z-index: 1; 108 | display: block; 109 | width: 100%; 110 | height: 20px; 111 | margin: 0; 112 | cursor: pointer; 113 | background: transparent; 114 | } 115 | 116 | .seek-bar { 117 | -webkit-appearance: none; 118 | -moz-appearance: none; 119 | appearance: none; 120 | position: relative; 121 | top: calc(50% - 10px); 122 | z-index: 1; 123 | display: block; 124 | width: 100%; 125 | height: 20px; 126 | margin: 0; 127 | cursor: pointer; 128 | background: transparent; 129 | } 130 | 131 | .seek-bar::-webkit-slider-runnable-track { 132 | -webkit-appearance: none; 133 | appearance: none; 134 | height: 6px; 135 | cursor: pointer; 136 | background: transparent; 137 | border: 0; 138 | border-radius: 0; 139 | } 140 | 141 | .seek-bar::-moz-range-track { 142 | -moz-appearance: none; 143 | appearance: none; 144 | width: 100%; 145 | height: 6px; 146 | cursor: pointer; 147 | background: transparent; 148 | border: 0; 149 | border-radius: 0; 150 | } 151 | 152 | .seek-bar::-webkit-slider-thumb { 153 | -webkit-appearance: none; 154 | appearance: none; 155 | width: 12px; 156 | height: 12px; 157 | margin-top: 3px; 158 | cursor: pointer; 159 | background: var(--seek-bar-thumb-color); 160 | border: 0; 161 | border-radius: 8px; 162 | transform: translateY(-50%); 163 | transition: opacity 0.4s ease-in-out; 164 | } 165 | 166 | .seek-bar::-moz-range-progress { 167 | margin-top: 0; 168 | background: transparent; 169 | border: 0; 170 | } 171 | 172 | .seek-bar::-moz-range-thumb { 173 | -moz-appearance: none; 174 | appearance: none; 175 | width: 16px; 176 | height: 16px; 177 | margin-top: 0; 178 | cursor: pointer; 179 | background: var(--seek-bar-thumb-color); 180 | border: 0; 181 | border-radius: 8px; 182 | transition: opacity 0.4s ease-in-out; 183 | } 184 | 185 | .seek-bar-wrapper ::-webkit-slider-thumb { 186 | opacity: 0; 187 | } 188 | 189 | .seek-bar-wrapper ::-moz-range-thumb { 190 | opacity: 0; 191 | } 192 | 193 | .seek-bar-wrapper:hover .seek-bar:not([disabled])::-webkit-slider-thumb { 194 | opacity: 1; 195 | } 196 | 197 | .seek-bar-wrapper:hover .seek-bar:not([disabled])::-moz-range-thumb { 198 | opacity: 1; 199 | } 200 | -------------------------------------------------------------------------------- /youtube-background-experimental.js: -------------------------------------------------------------------------------- 1 | /* youtube-background v1.1.8 | https://github.com/stamat/youtube-background | MIT License */ 2 | (() => { 3 | // src/lib/controls.js 4 | var SeekBar = class { 5 | constructor(element, vbgInstance) { 6 | this.lock = false; 7 | if (!element) 8 | return; 9 | this.element = element; 10 | if (this.element.hasAttribute("data-target-uid")) 11 | return; 12 | this.progressElem = this.element.querySelector(".js-seek-bar-progress"); 13 | this.inputElem = this.element.querySelector(".js-seek-bar"); 14 | this.targetSelector = this.element.getAttribute("data-target"); 15 | if (this.targetSelector) 16 | this.targetElem = document.querySelector(this.targetSelector); 17 | if (!this.targetSelector && vbgInstance) 18 | this.targetElem = vbgInstance.element; 19 | if (!this.targetElem) 20 | return; 21 | if (vbgInstance) 22 | this.setVBGInstance(vbgInstance); 23 | this.targetElem.addEventListener("video-background-time-update", this.onTimeUpdate.bind(this)); 24 | this.targetElem.addEventListener("video-background-play", this.onReady.bind(this)); 25 | this.targetElem.addEventListener("video-background-ready", this.onReady.bind(this)); 26 | this.targetElem.addEventListener("video-background-destroyed", this.onDestroyed.bind(this)); 27 | this.inputElem.addEventListener("input", this.onInput.bind(this)); 28 | this.inputElem.addEventListener("change", this.onChange.bind(this)); 29 | } 30 | setVBGInstance(vbgInstance) { 31 | if (this.vbgInstance) 32 | return; 33 | this.vbgInstance = vbgInstance; 34 | this.element.setAttribute("data-target-uid", vbgInstance.uid); 35 | } 36 | onReady(event) { 37 | this.setVBGInstance(event.detail); 38 | } 39 | onTimeUpdate(event) { 40 | this.setVBGInstance(event.detail); 41 | if (!this.lock) 42 | requestAnimationFrame(() => this.setProgress(this.vbgInstance.percentComplete)); 43 | } 44 | onDestroyed(event) { 45 | this.vbgInstance = null; 46 | requestAnimationFrame(() => this.setProgress(0)); 47 | } 48 | onInput(event) { 49 | this.lock = true; 50 | requestAnimationFrame(() => this.setProgress(event.target.value)); 51 | } 52 | onChange(event) { 53 | this.lock = false; 54 | requestAnimationFrame(() => this.setProgress(event.target.value)); 55 | if (this.vbgInstance) { 56 | this.vbgInstance.seek(event.target.value); 57 | if (this.vbgInstance.playerElement && this.vbgInstance.playerElement.style.opacity === 0) 58 | this.vbgInstance.playerElement.style.opacity = 1; 59 | } 60 | } 61 | setProgress(value) { 62 | if (this.progressElem) 63 | this.progressElem.value = value; 64 | if (this.inputElem) 65 | this.inputElem.value = value; 66 | } 67 | }; 68 | var VideoBackgroundGroup = class { 69 | constructor(selector, videoBackgroundSelector, videoBackgroundFactoryInstance) { 70 | this.element = selector; 71 | if (typeof this.element === "string") 72 | this.element = document.querySelector(selector); 73 | if (!this.element) 74 | return; 75 | this.elements = this.element.querySelectorAll(videoBackgroundSelector || "[data-vbg]"); 76 | if (!this.elements.length) 77 | return; 78 | this.videoBackgroundFactoryInstance = videoBackgroundFactoryInstance; 79 | this.stack = []; 80 | this.map = /* @__PURE__ */ new Map(); 81 | this.current = 0; 82 | this.currentElement = null; 83 | this.currentInstance = null; 84 | this.playing = false; 85 | this.muted = true; 86 | for (let i = 0; i < this.elements.length; i++) { 87 | const element = this.elements[i]; 88 | if (!element.hasAttribute("data-vbg-uid") && this.videoBackgroundFactoryInstance) 89 | this.videoBackgroundFactoryInstance.add(element); 90 | this.stack.push(element); 91 | this.map.set(element, i); 92 | if (i === 0) { 93 | this.current = 0; 94 | this.currentElement = element; 95 | if (this.videoBackgroundFactoryInstance) 96 | this.currentInstance = this.videoBackgroundFactoryInstance.get(element); 97 | } 98 | element.addEventListener("video-background-ended", this.onVideoEnded.bind(this)); 99 | element.addEventListener("video-background-seeked", this.onVideoSeeked.bind(this)); 100 | element.addEventListener("video-background-pause", this.onVideoPause.bind(this)); 101 | element.addEventListener("video-background-ready", this.onVideoReady.bind(this)); 102 | element.addEventListener("video-background-state-change", this.setVideoBackgroundFactoryInstance.bind(this), { once: true }); 103 | element.addEventListener("video-background-time-update", this.setVideoBackgroundFactoryInstance.bind(this), { once: true }); 104 | } 105 | } 106 | setVideoBackgroundFactoryInstance(event) { 107 | if (this.videoBackgroundFactoryInstance) 108 | return; 109 | this.videoBackgroundFactoryInstance = event.detail.factoryInstance; 110 | if (!this.currentInstance) 111 | this.currentInstance = this.videoBackgroundFactoryInstance.get(this.currentElement); 112 | } 113 | onVideoReady(event) { 114 | if (this.stack[this.current] !== event.detail.element) 115 | return; 116 | this.setVideoBackgroundFactoryInstance(event); 117 | const videoBackground = event.detail; 118 | if (videoBackground.params.muted) 119 | this.muted = true; 120 | if (!videoBackground.isIntersecting) 121 | return; 122 | if (!videoBackground.params.autoplay) 123 | return; 124 | this.playing = true; 125 | if (videoBackground.currentState === "playing") 126 | return; 127 | videoBackground.softPlay(); 128 | } 129 | onVideoPause(event) { 130 | ; 131 | this.setVideoBackgroundFactoryInstance(event); 132 | const stackIndex = this.map.get(event.detail.element); 133 | if (stackIndex === this.current) 134 | return; 135 | } 136 | levelSeekBars() { 137 | for (let i = 0; i < this.stack.length; i++) { 138 | if (i === this.current) 139 | continue; 140 | const seekBarElem = this.getSeekBar(this.videoBackgroundFactoryInstance.get(this.stack[i])); 141 | if (!seekBarElem) 142 | continue; 143 | if (i < this.current) { 144 | this.setProgress(seekBarElem, 100); 145 | } else { 146 | this.setProgress(seekBarElem, 0); 147 | } 148 | } 149 | } 150 | getSeekBar(currentInstance) { 151 | if (!currentInstance) 152 | return; 153 | const uid = currentInstance.uid; 154 | const element = document.querySelector(`.js-seek-bar-wrap[data-target-uid="${uid}"]`); 155 | if (!element) 156 | return; 157 | return element; 158 | } 159 | setProgress(seekBarElem, value) { 160 | if (!seekBarElem) 161 | return; 162 | const progressElem = seekBarElem.querySelector(".js-seek-bar-progress"); 163 | const inputElem = seekBarElem.querySelector(".js-seek-bar"); 164 | if (progressElem) 165 | progressElem.value = value; 166 | if (inputElem) 167 | inputElem.value = value; 168 | } 169 | onVideoSeeked(event) { 170 | const current = this.map.get(event.detail.element); 171 | if (this.current !== current) 172 | this.setCurrent(current, true); 173 | } 174 | setCurrent(index, seek) { 175 | const previous = this.current; 176 | if (index >= this.stack.length) 177 | index = 0; 178 | if (index < 0) 179 | index = this.stack.length - 1; 180 | const previousInstance = this.videoBackgroundFactoryInstance.get(this.stack[previous]); 181 | this.current = index; 182 | this.currentInstance = this.videoBackgroundFactoryInstance.get(this.stack[this.current]); 183 | this.currentElement = this.stack[this.current]; 184 | this.stack[previous].style.display = "none"; 185 | this.currentElement.style.display = "block"; 186 | if (!seek) { 187 | const seekBarElem = this.getSeekBar(this.currentInstance); 188 | if (seekBarElem) 189 | this.setProgress(seekBarElem, 0); 190 | this.currentInstance.seek(0); 191 | } 192 | setTimeout(() => { 193 | if (this.currentInstance.currentState !== "playing") 194 | this.currentInstance.play(); 195 | }, 100); 196 | if (previousInstance && previousInstance.currentState !== "paused") 197 | previousInstance.pause(); 198 | setTimeout(this.levelSeekBars.bind(this), 100); 199 | if (index >= this.stack.length) 200 | this.dispatchEvent("video-background-group-forward-rewind"); 201 | if (index < 0) 202 | this.dispatchEvent("video-background-group-backward-rewind"); 203 | } 204 | dispatchEvent(name) { 205 | this.element.dispatchEvent(new CustomEvent(name, { bubbles: true, detail: this })); 206 | } 207 | onVideoEnded(event) { 208 | if (event.detail.element !== this.currentElement) 209 | return; 210 | this.next(); 211 | } 212 | next() { 213 | this.setCurrent(this.current + 1); 214 | this.dispatchEvent("video-background-group-next"); 215 | } 216 | prev() { 217 | this.setCurrent(this.current - 1); 218 | this.dispatchEvent("video-background-group-previous"); 219 | } 220 | unmute() { 221 | for (let i = 0; i < this.stack.length; i++) { 222 | const instance = this.videoBackgroundFactoryInstance.get(this.stack[i]); 223 | if (!instance) 224 | continue; 225 | instance.unmute(); 226 | } 227 | this.muted = false; 228 | this.dispatchEvent("video-background-group-umnute"); 229 | } 230 | mute() { 231 | for (let i = 0; i < this.stack.length; i++) { 232 | const instance = this.videoBackgroundFactoryInstance.get(this.stack[i]); 233 | if (!instance) 234 | continue; 235 | instance.mute(); 236 | } 237 | this.muted = true; 238 | this.dispatchEvent("video-background-group-mute"); 239 | } 240 | pause() { 241 | this.currentInstance.pause(); 242 | this.playing = false; 243 | this.dispatchEvent("video-background-group-pause"); 244 | } 245 | play() { 246 | this.currentInstance.play(); 247 | this.playing = true; 248 | this.dispatchEvent("video-background-group-play"); 249 | } 250 | destroy() { 251 | for (let i = 0; i < this.elements.length; i++) { 252 | const element = this.elements[i]; 253 | element.removeEventListener("video-background-ended", this.onVideoEnded.bind(this)); 254 | element.removeEventListener("video-background-seeked", this.onVideoSeeked.bind(this)); 255 | element.removeEventListener("video-background-pause", this.onVideoPause.bind(this)); 256 | element.removeEventListener("video-background-ready", this.onVideoReady.bind(this)); 257 | element.removeEventListener("video-background-state-change", this.setVideoBackgroundFactoryInstance.bind(this)); 258 | element.removeEventListener("video-background-time-update", this.setVideoBackgroundFactoryInstance.bind(this)); 259 | } 260 | } 261 | }; 262 | var PlayToggle = class { 263 | constructor(playToggleElem, vbgInstance) { 264 | if (!playToggleElem) 265 | return; 266 | this.element = playToggleElem; 267 | this.targetSelector = this.element.getAttribute("data-target"); 268 | if (!this.targetSelector) 269 | return; 270 | this.active = false; 271 | if (this.element.hasAttribute("aria-pressed")) { 272 | this.active = this.element.getAttribute("aria-pressed") === "true"; 273 | } else { 274 | this.element.setAttribute("aria-pressed", this.active); 275 | } 276 | this.element.setAttribute("role", "switch"); 277 | this.targetElem = document.querySelector(this.targetSelector); 278 | if (!this.targetElem) 279 | return; 280 | if (vbgInstance) 281 | this.vbgInstance = vbgInstance; 282 | this.targetElem.addEventListener("video-background-ready", this.onReady.bind(this)); 283 | this.targetElem.addEventListener("video-background-state-change", this.onStateChange.bind(this)); 284 | this.targetElem.addEventListener("video-background-play", this.onPlay.bind(this)); 285 | this.targetElem.addEventListener("video-background-pause", this.onPause.bind(this)); 286 | this.targetElem.addEventListener("video-background-destroyed", this.onDestroyed.bind(this)); 287 | this.element.addEventListener("click", this.onClick.bind(this)); 288 | } 289 | onReady(event) { 290 | this.vbgInstance = event.detail; 291 | } 292 | onStateChange(event) { 293 | if (!this.vbgInstance) 294 | this.vbgInstance = event.detail; 295 | this.active = this.vbgInstance.currentState === "playing" || this.vbgInstance.currentState === "buffering"; 296 | this.element.setAttribute("aria-pressed", this.active); 297 | } 298 | onPlay(event) { 299 | if (!this.vbgInstance) 300 | this.vbgInstance = event.detail; 301 | this.active = true; 302 | this.element.setAttribute("aria-pressed", this.active); 303 | } 304 | onPause(event) { 305 | if (!this.vbgInstance) 306 | this.vbgInstance = event.detail; 307 | this.active = false; 308 | this.element.setAttribute("aria-pressed", this.active); 309 | } 310 | onDestroyed(event) { 311 | this.vbgInstance = null; 312 | this.active = false; 313 | this.element.setAttribute("aria-pressed", this.active); 314 | } 315 | onClick(event) { 316 | if (!this.vbgInstance) 317 | return; 318 | if (this.active) { 319 | this.vbgInstance.pause(); 320 | } else { 321 | this.vbgInstance.play(); 322 | } 323 | } 324 | }; 325 | var MuteToggle = class { 326 | constructor(muteToggleElem, vbgInstance) { 327 | if (!muteToggleElem) 328 | return; 329 | this.element = muteToggleElem; 330 | this.targetSelector = this.element.getAttribute("data-target"); 331 | if (!this.targetSelector) 332 | return; 333 | this.active = false; 334 | if (this.element.hasAttribute("aria-pressed")) { 335 | this.active = this.element.getAttribute("aria-pressed") === "true"; 336 | } else { 337 | this.element.setAttribute("aria-pressed", this.active); 338 | } 339 | this.element.setAttribute("role", "switch"); 340 | this.targetElem = document.querySelector(this.targetSelector); 341 | if (!this.targetElem) 342 | return; 343 | if (vbgInstance) 344 | this.vbgInstance = vbgInstance; 345 | this.targetElem.addEventListener("video-background-ready", this.onReady.bind(this)); 346 | this.targetElem.addEventListener("video-background-mute", this.onMute.bind(this)); 347 | this.targetElem.addEventListener("video-background-unmute", this.onUnmute.bind(this)); 348 | this.targetElem.addEventListener("video-background-destroyed", this.onDestroyed.bind(this)); 349 | this.element.addEventListener("click", this.onClick.bind(this)); 350 | } 351 | onReady(event) { 352 | this.vbgInstance = event.detail; 353 | if (this.vbgInstance.params.muted) { 354 | this.active = true; 355 | this.element.setAttribute("aria-pressed", this.active); 356 | } 357 | } 358 | onMute(event) { 359 | if (!this.vbgInstance) 360 | this.vbgInstance = event.detail; 361 | this.active = true; 362 | this.element.setAttribute("aria-pressed", this.active); 363 | } 364 | onUnmute(event) { 365 | if (!this.vbgInstance) 366 | this.vbgInstance = event.detail; 367 | this.active = false; 368 | this.element.setAttribute("aria-pressed", this.active); 369 | } 370 | onDestroyed(event) { 371 | this.vbgInstance = null; 372 | this.active = false; 373 | this.element.setAttribute("aria-pressed", this.active); 374 | } 375 | onClick(event) { 376 | if (!this.vbgInstance) 377 | return; 378 | if (this.active) { 379 | this.vbgInstance.unmute(); 380 | } else { 381 | this.vbgInstance.mute(); 382 | } 383 | } 384 | }; 385 | var GeneralFactory = class { 386 | constructor(selector, callback, uidAttribute = "data-uid") { 387 | this.instances = {}; 388 | this.selector = selector; 389 | this.elements = []; 390 | this.callback = callback; 391 | this.uidAttribute = uidAttribute; 392 | if (!callback || typeof callback !== "function") 393 | return; 394 | if (typeof this.selector === "string") { 395 | this.elements = document.querySelectorAll(this.selector); 396 | } 397 | if (this.selector instanceof Element) { 398 | this.elements = [this.selector]; 399 | } 400 | if (this.selector instanceof NodeList) { 401 | this.elements = this.selector; 402 | } 403 | for (let i = 0; i < this.elements.length; i++) { 404 | this.add(this.elements[i]); 405 | } 406 | } 407 | basicUID() { 408 | return Date.now().toString(36) + Math.random().toString(36).substring(2); 409 | } 410 | generateUID() { 411 | let tempuid = this.basicUID(); 412 | if (!this.instances.hasOwnProperty(tempuid)) 413 | return tempuid; 414 | return this.generateUID(); 415 | } 416 | add(element) { 417 | let id = element.getAttribute("id"); 418 | if (!id || this.instances.hasOwnProperty(id)) { 419 | id = element.getAttribute(this.uidAttribute); 420 | if (!id || this.instances.hasOwnProperty(id)) { 421 | id = this.generateUID(); 422 | element.setAttribute(this.uidAttribute, id); 423 | } 424 | } 425 | if (this.callback && typeof this.callback === "function") 426 | this.instances[id] = this.callback(element, id, this); 427 | } 428 | getID(element) { 429 | if (!element) 430 | return; 431 | if (typeof element === "string") 432 | return element; 433 | const id = element.getAttribute("id"); 434 | if (id && this.instances.hasOwnProperty(id)) 435 | return id; 436 | const uid = element.getAttribute(this.uidAttribute); 437 | if (uid && this.instances.hasOwnProperty(uid)) 438 | return uid; 439 | } 440 | get(element) { 441 | if (!element) 442 | return; 443 | const id = this.getID(element); 444 | if (!id) 445 | return; 446 | return this.instances[id]; 447 | } 448 | destroy(element) { 449 | if (!element) 450 | return; 451 | const id = this.getID(element); 452 | if (!id) 453 | return; 454 | const instance = this.instances[id]; 455 | if (instance.hasOwnProperty("destroy") && typeof instance.destroy == "function") 456 | this.instances[id].destroy(); 457 | delete this.instances[id]; 458 | } 459 | destroyAll() { 460 | for (const uid in this.instances) { 461 | const instance = this.instances[uid]; 462 | if (instance.hasOwnProperty("destroy") && typeof instance.destroy == "function") 463 | instance.destroy(); 464 | delete this.instances[uid]; 465 | } 466 | } 467 | }; 468 | var VideoBackgroundGroups = class extends GeneralFactory { 469 | constructor(selector = ".js-vbg-group", videoBackgroundSelector, videoBackgroundFactoryInstance) { 470 | super(selector, (element, id, factoryInstance) => new VideoBackgroundGroup(element, videoBackgroundSelector, videoBackgroundFactoryInstance)); 471 | } 472 | }; 473 | 474 | // src/experimental.js 475 | window.SeekBar = SeekBar; 476 | window.PlayToggle = PlayToggle; 477 | window.MuteToggle = MuteToggle; 478 | window.VideoBackgroundGroup = VideoBackgroundGroup; 479 | window.VideoBackgroundGroups = VideoBackgroundGroups; 480 | })(); 481 | //# sourceMappingURL=youtube-background-experimental.js.map 482 | -------------------------------------------------------------------------------- /youtube-background-experimental.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "sources": ["src/lib/controls.js", "src/experimental.js"], 4 | "sourcesContent": ["export class SeekBar {\n constructor(element, vbgInstance) {\n this.lock = false;\n if (!element) return;\n this.element = element;\n if (this.element.hasAttribute('data-target-uid')) return;\n this.progressElem = this.element.querySelector('.js-seek-bar-progress');\n this.inputElem = this.element.querySelector('.js-seek-bar');\n this.targetSelector = this.element.getAttribute('data-target');\n if (this.targetSelector) this.targetElem = document.querySelector(this.targetSelector);\n if (!this.targetSelector && vbgInstance) this.targetElem = vbgInstance.element;\n if (!this.targetElem) return;\n\n if (vbgInstance) this.setVBGInstance(vbgInstance);\n \n this.targetElem.addEventListener('video-background-time-update', this.onTimeUpdate.bind(this));\n this.targetElem.addEventListener('video-background-play', this.onReady.bind(this));\n this.targetElem.addEventListener('video-background-ready', this.onReady.bind(this));\n this.targetElem.addEventListener('video-background-destroyed', this.onDestroyed.bind(this));\n\n this.inputElem.addEventListener('input', this.onInput.bind(this));\n this.inputElem.addEventListener('change', this.onChange.bind(this));\n }\n\n setVBGInstance(vbgInstance) {\n if (this.vbgInstance) return;\n this.vbgInstance = vbgInstance;\n this.element.setAttribute('data-target-uid', vbgInstance.uid);\n }\n\n onReady(event) {\n this.setVBGInstance(event.detail);\n }\n\n onTimeUpdate(event) {\n this.setVBGInstance(event.detail);\n if (!this.lock) requestAnimationFrame(() => this.setProgress(this.vbgInstance.percentComplete));\n }\n\n onDestroyed(event) {\n this.vbgInstance = null;\n requestAnimationFrame(() => this.setProgress(0));\n }\n\n onInput(event) {\n this.lock = true;\n requestAnimationFrame(() => this.setProgress(event.target.value));\n }\n\n onChange(event) {\n this.lock = false;\n requestAnimationFrame(() => this.setProgress(event.target.value));\n if (this.vbgInstance) {\n this.vbgInstance.seek(event.target.value);\n if (this.vbgInstance.playerElement && this.vbgInstance.playerElement.style.opacity === 0) this.vbgInstance.playerElement.style.opacity = 1;\n }\n}\n\n setProgress(value) {\n if (this.progressElem) this.progressElem.value = value;\n if (this.inputElem) this.inputElem.value = value;\n }\n}\n\nexport class VideoBackgroundGroup {\n constructor(selector, videoBackgroundSelector, videoBackgroundFactoryInstance) {\n this.element = selector;\n if (typeof this.element === 'string') this.element = document.querySelector(selector);\n if (!this.element) return;\n this.elements = this.element.querySelectorAll(videoBackgroundSelector || '[data-vbg]');\n if (!this.elements.length) return;\n\n this.videoBackgroundFactoryInstance = videoBackgroundFactoryInstance;\n this.stack = [];\n this.map = new Map();\n this.current = 0;\n this.currentElement = null;\n this.currentInstance = null;\n\n this.playing = false;\n this.muted = true;\n\n for (let i = 0; i < this.elements.length; i++) {\n const element = this.elements[i];\n if (!element.hasAttribute('data-vbg-uid') && this.videoBackgroundFactoryInstance) this.videoBackgroundFactoryInstance.add(element);\n this.stack.push(element);\n this.map.set(element, i);\n \n if (i === 0) {\n this.current = 0;\n this.currentElement = element;\n if (this.videoBackgroundFactoryInstance) this.currentInstance = this.videoBackgroundFactoryInstance.get(element);\n }\n element.addEventListener('video-background-ended', this.onVideoEnded.bind(this));\n element.addEventListener('video-background-seeked', this.onVideoSeeked.bind(this));\n element.addEventListener('video-background-pause', this.onVideoPause.bind(this));\n element.addEventListener('video-background-ready', this.onVideoReady.bind(this));\n element.addEventListener('video-background-state-change', this.setVideoBackgroundFactoryInstance.bind(this), { once: true });\n element.addEventListener('video-background-time-update', this.setVideoBackgroundFactoryInstance.bind(this), { once: true });\n }\n }\n\n setVideoBackgroundFactoryInstance(event) {\n if (this.videoBackgroundFactoryInstance) return;\n this.videoBackgroundFactoryInstance = event.detail.factoryInstance;\n if (!this.currentInstance) this.currentInstance = this.videoBackgroundFactoryInstance.get(this.currentElement);\n }\n\n onVideoReady(event) {\n if (this.stack[this.current] !== event.detail.element) return;\n this.setVideoBackgroundFactoryInstance(event);\n const videoBackground = event.detail;\n if (videoBackground.params.muted) this.muted = true;\n if (!videoBackground.isIntersecting) return;\n if (!videoBackground.params.autoplay) return;\n this.playing = true;\n if (videoBackground.currentState === 'playing') return;\n videoBackground.softPlay();\n }\n\n onVideoPause(event) {;\n this.setVideoBackgroundFactoryInstance(event);\n const stackIndex = this.map.get(event.detail.element);\n if (stackIndex === this.current) return;\n }\n\n levelSeekBars() {\n for (let i = 0; i < this.stack.length; i++) {\n if (i === this.current) continue;\n const seekBarElem = this.getSeekBar(this.videoBackgroundFactoryInstance.get(this.stack[i]));\n if (!seekBarElem) continue;\n if (i < this.current) {\n this.setProgress(seekBarElem, 100);\n } else {\n this.setProgress(seekBarElem, 0);\n }\n }\n }\n\n getSeekBar(currentInstance) {\n if (!currentInstance) return;\n const uid = currentInstance.uid;\n const element = document.querySelector(`.js-seek-bar-wrap[data-target-uid=\"${uid}\"]`);\n if (!element) return;\n return element;\n }\n\n setProgress(seekBarElem, value) {\n if (!seekBarElem) return;\n const progressElem = seekBarElem.querySelector('.js-seek-bar-progress');\n const inputElem = seekBarElem.querySelector('.js-seek-bar');\n if (progressElem) progressElem.value = value;\n if (inputElem) inputElem.value = value;\n }\n\n onVideoSeeked(event) {\n const current = this.map.get(event.detail.element);\n if (this.current !== current) this.setCurrent(current, true);\n }\n\n setCurrent(index, seek) {\n const previous = this.current;\n if (index >= this.stack.length) index = 0;\n if (index < 0) index = this.stack.length - 1;\n const previousInstance = this.videoBackgroundFactoryInstance.get(this.stack[previous]);\n this.current = index;\n this.currentInstance = this.videoBackgroundFactoryInstance.get(this.stack[this.current]);\n this.currentElement = this.stack[this.current];\n \n this.stack[previous].style.display = 'none';\n this.currentElement.style.display = 'block';\n\n if (!seek) {\n const seekBarElem = this.getSeekBar(this.currentInstance);\n if (seekBarElem) this.setProgress(seekBarElem, 0);\n this.currentInstance.seek(0);\n }\n\n setTimeout(() => {\n if (this.currentInstance.currentState !== 'playing') this.currentInstance.play();\n }, 100);\n if (previousInstance && previousInstance.currentState !== 'paused') previousInstance.pause();\n\n setTimeout(this.levelSeekBars.bind(this), 100);\n\n if (index >= this.stack.length) this.dispatchEvent('video-background-group-forward-rewind');\n if (index < 0) this.dispatchEvent('video-background-group-backward-rewind');\n }\n\n dispatchEvent(name) {\n this.element.dispatchEvent(new CustomEvent(name, { bubbles: true, detail: this }));\n }\n\n onVideoEnded(event) {\n if (event.detail.element !== this.currentElement) return;\n this.next();\n }\n\n next() {\n this.setCurrent(this.current + 1);\n this.dispatchEvent('video-background-group-next');\n }\n\n prev() {\n this.setCurrent(this.current - 1);\n this.dispatchEvent('video-background-group-previous');\n }\n\n unmute() {\n for (let i = 0; i < this.stack.length; i++) {\n const instance = this.videoBackgroundFactoryInstance.get(this.stack[i]);\n if (!instance) continue;\n instance.unmute();\n }\n\n this.muted = false;\n this.dispatchEvent('video-background-group-umnute');\n }\n\n mute() {\n for (let i = 0; i < this.stack.length; i++) {\n const instance = this.videoBackgroundFactoryInstance.get(this.stack[i]);\n if (!instance) continue;\n instance.mute();\n }\n\n this.muted = true;\n this.dispatchEvent('video-background-group-mute');\n }\n\n pause() {\n this.currentInstance.pause();\n this.playing = false;\n this.dispatchEvent('video-background-group-pause');\n }\n\n play() {\n this.currentInstance.play();\n this.playing = true;\n this.dispatchEvent('video-background-group-play');\n }\n\n destroy() {\n for (let i = 0; i < this.elements.length; i++) {\n const element = this.elements[i];\n element.removeEventListener('video-background-ended', this.onVideoEnded.bind(this));\n element.removeEventListener('video-background-seeked', this.onVideoSeeked.bind(this));\n element.removeEventListener('video-background-pause', this.onVideoPause.bind(this));\n element.removeEventListener('video-background-ready', this.onVideoReady.bind(this));\n element.removeEventListener('video-background-state-change', this.setVideoBackgroundFactoryInstance.bind(this));\n element.removeEventListener('video-background-time-update', this.setVideoBackgroundFactoryInstance.bind(this));\n }\n }\n}\n\nexport class PlayToggle {\n constructor(playToggleElem, vbgInstance) {\n if (!playToggleElem) return;\n \n this.element = playToggleElem;\n this.targetSelector = this.element.getAttribute('data-target');\n\n if (!this.targetSelector) return;\n this.active = false;\n\n if (this.element.hasAttribute('aria-pressed')) {\n this.active = this.element.getAttribute('aria-pressed') === 'true';\n } else {\n this.element.setAttribute('aria-pressed', this.active);\n }\n\n this.element.setAttribute('role', 'switch');\n\n this.targetElem = document.querySelector(this.targetSelector);\n if (!this.targetElem) return;\n\n if (vbgInstance) this.vbgInstance = vbgInstance;\n\n this.targetElem.addEventListener('video-background-ready', this.onReady.bind(this));\n this.targetElem.addEventListener('video-background-state-change', this.onStateChange.bind(this));\n this.targetElem.addEventListener('video-background-play', this.onPlay.bind(this));\n this.targetElem.addEventListener('video-background-pause', this.onPause.bind(this));\n this.targetElem.addEventListener('video-background-destroyed', this.onDestroyed.bind(this));\n\n this.element.addEventListener('click', this.onClick.bind(this));\n }\n\n onReady(event) {\n this.vbgInstance = event.detail;\n }\n\n onStateChange(event) {\n if (!this.vbgInstance) this.vbgInstance = event.detail;\n this.active = this.vbgInstance.currentState === 'playing' || this.vbgInstance.currentState === 'buffering';\n this.element.setAttribute('aria-pressed', this.active);\n }\n\n onPlay(event) {\n if (!this.vbgInstance) this.vbgInstance = event.detail;\n this.active = true;\n this.element.setAttribute('aria-pressed', this.active);\n }\n\n onPause(event) {\n if (!this.vbgInstance) this.vbgInstance = event.detail;\n this.active = false;\n this.element.setAttribute('aria-pressed', this.active);\n }\n\n onDestroyed(event) {\n this.vbgInstance = null;\n this.active = false;\n this.element.setAttribute('aria-pressed', this.active);\n }\n\n onClick(event) {\n if (!this.vbgInstance) return;\n if (this.active) {\n this.vbgInstance.pause();\n } else {\n this.vbgInstance.play();\n }\n }\n}\n\nexport class MuteToggle {\n constructor(muteToggleElem, vbgInstance) {\n if (!muteToggleElem) return;\n \n this.element = muteToggleElem;\n this.targetSelector = this.element.getAttribute('data-target');\n\n if (!this.targetSelector) return;\n this.active = false;\n\n if (this.element.hasAttribute('aria-pressed')) {\n this.active = this.element.getAttribute('aria-pressed') === 'true';\n } else {\n this.element.setAttribute('aria-pressed', this.active);\n }\n\n this.element.setAttribute('role', 'switch');\n\n this.targetElem = document.querySelector(this.targetSelector);\n if (!this.targetElem) return;\n\n if (vbgInstance) this.vbgInstance = vbgInstance;\n\n this.targetElem.addEventListener('video-background-ready', this.onReady.bind(this));\n this.targetElem.addEventListener('video-background-mute', this.onMute.bind(this));\n this.targetElem.addEventListener('video-background-unmute', this.onUnmute.bind(this));\n this.targetElem.addEventListener('video-background-destroyed', this.onDestroyed.bind(this));\n\n this.element.addEventListener('click', this.onClick.bind(this));\n }\n\n onReady(event) {\n this.vbgInstance = event.detail;\n if (this.vbgInstance.params.muted) {\n this.active = true;\n this.element.setAttribute('aria-pressed', this.active);\n }\n }\n\n onMute(event) {\n if (!this.vbgInstance) this.vbgInstance = event.detail;\n this.active = true;\n this.element.setAttribute('aria-pressed', this.active);\n }\n\n onUnmute(event) {\n if (!this.vbgInstance) this.vbgInstance = event.detail;\n this.active = false;\n this.element.setAttribute('aria-pressed', this.active);\n }\n\n onDestroyed(event) {\n this.vbgInstance = null;\n this.active = false;\n this.element.setAttribute('aria-pressed', this.active);\n }\n\n onClick(event) {\n if (!this.vbgInstance) return;\n if (this.active) {\n this.vbgInstance.unmute();\n } else {\n this.vbgInstance.mute();\n }\n }\n}\n\n// TODO: this can be achieved with custom elements... Maybe it's finally time to use them?\nclass GeneralFactory {\n constructor(selector, callback, uidAttribute = 'data-uid') {\n this.instances = {};\n this.selector = selector;\n this.elements = [];\n this.callback = callback;\n this.uidAttribute = uidAttribute;\n\n if (!callback || typeof callback !== 'function') return;\n\n if (typeof this.selector === 'string') {\n this.elements = document.querySelectorAll(this.selector);\n }\n\n if (this.selector instanceof Element) {\n this.elements = [this.selector];\n }\n\n if (this.selector instanceof NodeList) {\n this.elements = this.selector;\n }\n\n for (let i = 0; i < this.elements.length; i++) {\n this.add(this.elements[i]);\n\n // TODO: maybe manage elements array?\n }\n }\n\n basicUID() {\n return Date.now().toString(36) + Math.random().toString(36).substring(2);\n }\n\n generateUID() {\n let tempuid = this.basicUID();\n if (!this.instances.hasOwnProperty(tempuid)) return tempuid;\n return this.generateUID();\n }\n\n add(element) {\n let id = element.getAttribute('id');\n if (!id || this.instances.hasOwnProperty(id)) {\n id = element.getAttribute(this.uidAttribute);\n\n if (!id || this.instances.hasOwnProperty(id)) {\n id = this.generateUID();\n element.setAttribute(this.uidAttribute, id);\n }\n }\n\n if (this.callback && typeof this.callback === 'function') \n this.instances[id] = this.callback(element, id, this);\n }\n\n getID(element) {\n if (!element) return;\n if (typeof element === 'string') return element;\n const id = element.getAttribute('id');\n if (id && this.instances.hasOwnProperty(id)) return id;\n const uid = element.getAttribute(this.uidAttribute);\n if (uid && this.instances.hasOwnProperty(uid)) return uid;\n }\n\n get(element) {\n if (!element) return;\n const id = this.getID(element);\n if (!id) return;\n return this.instances[id];\n }\n\n destroy(element) {\n if (!element) return;\n const id = this.getID(element);\n if (!id) return;\n const instance = this.instances[id];\n if (instance.hasOwnProperty('destroy') && typeof instance.destroy == 'function') this.instances[id].destroy();\n delete this.instances[id];\n }\n\n destroyAll() {\n for (const uid in this.instances) {\n const instance = this.instances[uid];\n if (instance.hasOwnProperty('destroy') && typeof instance.destroy == 'function') instance.destroy();\n delete this.instances[uid];\n }\n }\n}\n\nexport class VideoBackgroundGroups extends GeneralFactory {\n constructor(selector = '.js-vbg-group', videoBackgroundSelector, videoBackgroundFactoryInstance) {\n super(selector, (element, id, factoryInstance) => new VideoBackgroundGroup(element, videoBackgroundSelector, videoBackgroundFactoryInstance));\n }\n}\n", "import { SeekBar, PlayToggle, MuteToggle, VideoBackgroundGroup, VideoBackgroundGroups } from \"./lib/controls\";\n\nwindow.SeekBar = SeekBar;\nwindow.PlayToggle = PlayToggle;\nwindow.MuteToggle = MuteToggle;\nwindow.VideoBackgroundGroup = VideoBackgroundGroup;\nwindow.VideoBackgroundGroups = VideoBackgroundGroups;\n"], 5 | "mappings": "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", 6 | "names": [] 7 | } 8 | -------------------------------------------------------------------------------- /youtube-background-experimental.min.js: -------------------------------------------------------------------------------- 1 | /* youtube-background v1.1.8 | https://github.com/stamat/youtube-background | MIT License */ 2 | (()=>{var a=class{constructor(t,e){this.lock=!1,t&&(this.element=t,!this.element.hasAttribute("data-target-uid")&&(this.progressElem=this.element.querySelector(".js-seek-bar-progress"),this.inputElem=this.element.querySelector(".js-seek-bar"),this.targetSelector=this.element.getAttribute("data-target"),this.targetSelector&&(this.targetElem=document.querySelector(this.targetSelector)),!this.targetSelector&&e&&(this.targetElem=e.element),this.targetElem&&(e&&this.setVBGInstance(e),this.targetElem.addEventListener("video-background-time-update",this.onTimeUpdate.bind(this)),this.targetElem.addEventListener("video-background-play",this.onReady.bind(this)),this.targetElem.addEventListener("video-background-ready",this.onReady.bind(this)),this.targetElem.addEventListener("video-background-destroyed",this.onDestroyed.bind(this)),this.inputElem.addEventListener("input",this.onInput.bind(this)),this.inputElem.addEventListener("change",this.onChange.bind(this)))))}setVBGInstance(t){this.vbgInstance||(this.vbgInstance=t,this.element.setAttribute("data-target-uid",t.uid))}onReady(t){this.setVBGInstance(t.detail)}onTimeUpdate(t){this.setVBGInstance(t.detail),this.lock||requestAnimationFrame(()=>this.setProgress(this.vbgInstance.percentComplete))}onDestroyed(t){this.vbgInstance=null,requestAnimationFrame(()=>this.setProgress(0))}onInput(t){this.lock=!0,requestAnimationFrame(()=>this.setProgress(t.target.value))}onChange(t){this.lock=!1,requestAnimationFrame(()=>this.setProgress(t.target.value)),this.vbgInstance&&(this.vbgInstance.seek(t.target.value),this.vbgInstance.playerElement&&this.vbgInstance.playerElement.style.opacity===0&&(this.vbgInstance.playerElement.style.opacity=1))}setProgress(t){this.progressElem&&(this.progressElem.value=t),this.inputElem&&(this.inputElem.value=t)}},r=class{constructor(t,e,s){if(this.element=t,typeof this.element=="string"&&(this.element=document.querySelector(t)),!!this.element&&(this.elements=this.element.querySelectorAll(e||"[data-vbg]"),!!this.elements.length)){this.videoBackgroundFactoryInstance=s,this.stack=[],this.map=new Map,this.current=0,this.currentElement=null,this.currentInstance=null,this.playing=!1,this.muted=!0;for(let i=0;i=this.stack.length&&(t=0),t<0&&(t=this.stack.length-1);const i=this.videoBackgroundFactoryInstance.get(this.stack[s]);if(this.current=t,this.currentInstance=this.videoBackgroundFactoryInstance.get(this.stack[this.current]),this.currentElement=this.stack[this.current],this.stack[s].style.display="none",this.currentElement.style.display="block",!e){const n=this.getSeekBar(this.currentInstance);n&&this.setProgress(n,0),this.currentInstance.seek(0)}setTimeout(()=>{this.currentInstance.currentState!=="playing"&&this.currentInstance.play()},100),i&&i.currentState!=="paused"&&i.pause(),setTimeout(this.levelSeekBars.bind(this),100),t>=this.stack.length&&this.dispatchEvent("video-background-group-forward-rewind"),t<0&&this.dispatchEvent("video-background-group-backward-rewind")}dispatchEvent(t){this.element.dispatchEvent(new CustomEvent(t,{bubbles:!0,detail:this}))}onVideoEnded(t){t.detail.element===this.currentElement&&this.next()}next(){this.setCurrent(this.current+1),this.dispatchEvent("video-background-group-next")}prev(){this.setCurrent(this.current-1),this.dispatchEvent("video-background-group-previous")}unmute(){for(let t=0;tnew r(i,e,s))}};window.SeekBar=a,window.PlayToggle=o,window.MuteToggle=h,window.VideoBackgroundGroup=r,window.VideoBackgroundGroups=d})(); 3 | --------------------------------------------------------------------------------