├── .editorconfig ├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .npmignore ├── .nvmrc ├── .travis.yml ├── .whitesource ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── scripts ├── karma.conf.js ├── postcss.config.js └── rollup.config.js ├── src ├── index.js ├── overlay-component.js ├── plugin.js └── plugin.scss └── test └── plugin.test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_style = space 8 | indent_size = 2 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | Briefly describe the issue. 3 | Include a [reduced test case](https://css-tricks.com/reduced-test-cases/). 4 | 5 | ## Steps to reproduce 6 | Explain in detail the exact steps necessary to reproduce the issue. 7 | 8 | 1. 9 | 2. 10 | 3. 11 | 12 | ## Results 13 | ### Expected 14 | Please describe what you expected to see. 15 | 16 | ### Actual 17 | Please describe what actually happened. 18 | 19 | ### Error output 20 | If there are any errors at all, please include them here. 21 | 22 | ## Additional Information 23 | Please include any additional information necessary here. Including the following: 24 | 25 | ### versions 26 | #### videojs 27 | what version of videojs does this occur with? 28 | 29 | #### browsers 30 | what browser are affected? 31 | 32 | #### OSes 33 | what platforms (operating systems and devices) are affected? 34 | 35 | ### plugins 36 | are any videojs plugins being used on the page? If so, please list them below. 37 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | Please describe the change as necessary. 3 | If it's a feature or enhancement please be as detailed as possible. 4 | If it's a bug fix, please link the issue that it fixes or describe the bug in as much detail. 5 | 6 | ## Specific Changes proposed 7 | Please list the specific changes involved in this pull request. 8 | 9 | ## Requirements Checklist 10 | - [ ] Feature implemented / Bug fixed 11 | - [ ] If necessary, more likely in a feature request than a bug fix 12 | - [ ] Unit Tests updated or fixed 13 | - [ ] Docs/guides updated 14 | - [ ] Reviewed by Two Core Contributors 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS 2 | Thumbs.db 3 | ehthumbs.db 4 | Desktop.ini 5 | .DS_Store 6 | ._* 7 | 8 | # Editors 9 | *~ 10 | *.swp 11 | *.tmproj 12 | *.tmproject 13 | *.sublime-* 14 | .idea/ 15 | .project/ 16 | .settings/ 17 | .vscode/ 18 | 19 | # Logs 20 | logs 21 | *.log 22 | npm-debug.log* 23 | 24 | # Dependency directories 25 | bower_components/ 26 | node_modules/ 27 | 28 | # Build-related directories 29 | dist/ 30 | docs/api/ 31 | test/dist/ 32 | .eslintcache 33 | .yo-rc.json 34 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Intentionally left blank, so that npm does not ignore anything by default, 2 | # but relies on the package.json "files" array to explicitly define what ends 3 | # up in the package. 4 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/* 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | dist: trusty 3 | language: node_js 4 | # node version is specified using the .nvmrc file 5 | before_install: 6 | - npm install -g greenkeeper-lockfile@1 7 | before_script: 8 | - export DISPLAY=:99.0 9 | - sh -e /etc/init.d/xvfb start 10 | - greenkeeper-lockfile-update 11 | after_script: 12 | - greenkeeper-lockfile-upload 13 | addons: 14 | firefox: latest 15 | chrome: stable 16 | 17 | -------------------------------------------------------------------------------- /.whitesource: -------------------------------------------------------------------------------- 1 | { 2 | "settingsInheritedFrom": "brightcove/whitesource-config@main" 3 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # [4.0.0](https://github.com/brightcove/videojs-overlay/compare/v3.1.0...v4.0.0) (2024-10-28) 3 | 4 | ### Features 5 | 6 | * move to advanced plugin and add reset API (#224) ([8524c38](https://github.com/brightcove/videojs-overlay/commit/8524c38)), closes [#224](https://github.com/brightcove/videojs-overlay/issues/224) 7 | 8 | ### Bug Fixes 9 | 10 | * package json export for publish (#225) ([944acd6](https://github.com/brightcove/videojs-overlay/commit/944acd6)), closes [#225](https://github.com/brightcove/videojs-overlay/issues/225) 11 | 12 | 13 | # [3.1.0](https://github.com/brightcove/videojs-overlay/compare/v3.0.0...v3.1.0) (2023-06-15) 14 | 15 | ### Features 16 | 17 | * make overlays mutable (#223) ([a6c0353](https://github.com/brightcove/videojs-overlay/commit/a6c0353)), closes [#223](https://github.com/brightcove/videojs-overlay/issues/223) 18 | 19 | 20 | # [3.0.0](https://github.com/brightcove/videojs-overlay/compare/v2.1.5...v3.0.0) (2022-12-16) 21 | 22 | ### Chores 23 | 24 | * skip vjsverify es check ([e10a7b1](https://github.com/brightcove/videojs-overlay/commit/e10a7b1)) 25 | * update build tooling to drop older browser support (#220) ([85fe717](https://github.com/brightcove/videojs-overlay/commit/85fe717)), closes [#220](https://github.com/brightcove/videojs-overlay/issues/220) 26 | 27 | 28 | ### BREAKING CHANGES 29 | 30 | * This removes support for some older browsers such as IE 11 31 | 32 | 33 | ## [2.1.5](https://github.com/brightcove/videojs-overlay/compare/v2.1.4...v2.1.5) (2021-09-13) 34 | 35 | ### Bug Fixes 36 | 37 | * qualityMenu button to left of playToggle (#118) ([952188e](https://github.com/brightcove/videojs-overlay/commit/952188e)), closes [#118](https://github.com/brightcove/videojs-overlay/issues/118) 38 | 39 | ### Chores 40 | 41 | * **package:** update lint-staged to version 8.1.0 (#94) ([b3cee2f](https://github.com/brightcove/videojs-overlay/commit/b3cee2f)), closes [#94](https://github.com/brightcove/videojs-overlay/issues/94) 42 | * **package:** update npm-run-all/videojs-generator-verify for security ([9d2d40f](https://github.com/brightcove/videojs-overlay/commit/9d2d40f)) 43 | * **package:** update videojs-generate-karma-config to version 5.0.0 (#93) ([4e9d161](https://github.com/brightcove/videojs-overlay/commit/4e9d161)), closes [#93](https://github.com/brightcove/videojs-overlay/issues/93) 44 | * **package:** update videojs-generate-rollup-config to version 2.3.1 (#95) ([dedba7c](https://github.com/brightcove/videojs-overlay/commit/dedba7c)), closes [#95](https://github.com/brightcove/videojs-overlay/issues/95) 45 | * **package:** update videojs-standard to version 8.0.2 (#96) ([b548b3b](https://github.com/brightcove/videojs-overlay/commit/b548b3b)), closes [#96](https://github.com/brightcove/videojs-overlay/issues/96) 46 | 47 | 48 | ## [2.1.4](https://github.com/brightcove/videojs-overlay/compare/v2.1.3...v2.1.4) (2018-09-19) 49 | 50 | ### Bug Fixes 51 | 52 | * Properly expose plugin version (#80) ([9c8822c](https://github.com/brightcove/videojs-overlay/commit/9c8822c)), closes [#80](https://github.com/brightcove/videojs-overlay/issues/80) 53 | * Remove the postinstall script to prevent install issues (#77) ([5edecf7](https://github.com/brightcove/videojs-overlay/commit/5edecf7)), closes [#77](https://github.com/brightcove/videojs-overlay/issues/77) 54 | 55 | ### Chores 56 | 57 | * update to generator-videojs-plugin[@7](https://github.com/7).2.0 ([7e0b357](https://github.com/brightcove/videojs-overlay/commit/7e0b357)) 58 | 59 | 60 | ## [2.1.3](https://github.com/brightcove/videojs-overlay/compare/v2.1.2...v2.1.3) (2018-08-23) 61 | 62 | ### Chores 63 | 64 | * generator v7 (#73) ([449679e](https://github.com/brightcove/videojs-overlay/commit/449679e)), closes [#73](https://github.com/brightcove/videojs-overlay/issues/73) 65 | 66 | 67 | ## [2.1.2](https://github.com/brightcove/videojs-overlay/compare/v2.1.1...v2.1.2) (2018-08-03) 68 | 69 | ### Bug Fixes 70 | 71 | * babel the es dist, by updating the generator (#68) ([bd7f070](https://github.com/brightcove/videojs-overlay/commit/bd7f070)), closes [#68](https://github.com/brightcove/videojs-overlay/issues/68) 72 | 73 | ### Chores 74 | 75 | * **package:** update dependencies, enable greenkeeper (#67) ([70afc00](https://github.com/brightcove/videojs-overlay/commit/70afc00)), closes [#67](https://github.com/brightcove/videojs-overlay/issues/67) 76 | 77 | 78 | ## [2.1.1](https://github.com/brightcove/videojs-overlay/compare/v2.1.0...v2.1.1) (2018-07-05) 79 | 80 | ### Chores 81 | 82 | * update to generator v6 (#63) ([4ac2452](https://github.com/brightcove/videojs-overlay/commit/4ac2452)), closes [#63](https://github.com/brightcove/videojs-overlay/issues/63) 83 | 84 | 85 | # [2.1.0](https://github.com/brightcove/videojs-overlay/compare/v2.0.0...v2.1.0) (2018-04-20) 86 | 87 | ### Features 88 | 89 | * Allow choosing the placement of overlay elements in the control bar. ([b8b0607](https://github.com/brightcove/videojs-overlay/commit/b8b0607)) 90 | 91 | ### Bug Fixes 92 | 93 | * Upgrade rollup to v0.52.x to fix build failures ([#60](https://github.com/brightcove/videojs-overlay/issues/60)) ([b0b3a5d](https://github.com/brightcove/videojs-overlay/commit/b0b3a5d)) 94 | 95 | 96 | # [2.0.0](https://github.com/brightcove/videojs-overlay/compare/v1.1.3...v2.0.0) (2017-08-24) 97 | 98 | ### Features 99 | 100 | * Fix vertical centre alignment and add align-center ([#38](https://github.com/brightcove/videojs-overlay/issues/38)) ([8649210](https://github.com/brightcove/videojs-overlay/commit/8649210)) 101 | 102 | ### Bug Fixes 103 | 104 | * Fix malformed README link ([#43](https://github.com/brightcove/videojs-overlay/issues/43)) ([c2b1315](https://github.com/brightcove/videojs-overlay/commit/c2b1315)) 105 | * remove global browserify transforms, so parent packages don't break ([#48](https://github.com/brightcove/videojs-overlay/issues/48)) ([aa74853](https://github.com/brightcove/videojs-overlay/commit/aa74853)) 106 | 107 | ### Code Refactoring 108 | 109 | * Update to use generator v5 tooling. ([#51](https://github.com/brightcove/videojs-overlay/issues/51)) ([bfeff8c](https://github.com/brightcove/videojs-overlay/commit/bfeff8c)) 110 | 111 | ## 1.1.4 (2017-04-03) 112 | * fix: remove global browserify transforms, so parent packages don't break 113 | 114 | ## 1.1.3 (2017-02-27) 115 | * update travis to test vjs 5/6 (#46) 116 | 117 | ## 1.1.2 (2017-02-03) 118 | * Added Video.js 5 and 6 cross-compatibility. 119 | 120 | ## 1.1.1 (2016-08-05) 121 | * Fixed issue where max-width was being set on all overlays rather than only those showBackground=false. 122 | 123 | ## 1.1.0 (2016-07-27) 124 | * Added showBackground option to show or hide the overlay background. 125 | * Added attachToControlBar option to allow bottom align control bars to move when the control bar minimizes. 126 | 127 | ## 1.0.2 (2016-06-10) 128 | _(none)_ 129 | 130 | ## 1.0.1 (2016-03-08) 131 | * Fixed #22, should not have been checking for integers only. 132 | 133 | ## 1.0.0 (2016-02-12) 134 | * Major refactoring of plugin to align with generator-videojs-plugin standards. 135 | * Fixed significant edge-case issues with creation/destruction of overlays. 136 | 137 | ## 0.1.0 (2014-04-29) 138 | * Initial release 139 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # CONTRIBUTING 2 | 3 | We welcome contributions from everyone! 4 | 5 | ## Getting Started 6 | 7 | Make sure you have Node.js 4.8 or higher and npm installed. 8 | 9 | 1. Fork this repository and clone your fork 10 | 1. Install dependencies: `npm install` 11 | 1. Run a development server: `npm start` 12 | 13 | ### Making Changes 14 | 15 | Refer to the [video.js plugin conventions][conventions] for more detail on best practices and tooling for video.js plugin authorship. 16 | 17 | When you've made your changes, push your commit(s) to your fork and issue a pull request against the original repository. 18 | 19 | ### Running Tests 20 | 21 | Testing is a crucial part of any software project. For all but the most trivial changes (typos, etc) test cases are expected. Tests are run in actual browsers using [Karma][karma]. 22 | 23 | - In all available and supported browsers: `npm test` 24 | - In a specific browser: `npm run test:chrome`, `npm run test:firefox`, etc. 25 | - While development server is running (`npm start`), navigate to [`http://localhost:9999/test/`][local] 26 | 27 | 28 | [karma]: http://karma-runner.github.io/ 29 | [local]: http://localhost:9999/test/ 30 | [conventions]: https://github.com/videojs/generator-videojs-plugin/blob/master/docs/conventions.md 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Brightcove, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # videojs-overlay 2 | 3 | [![Build Status](https://travis-ci.org/brightcove/videojs-overlay.svg?branch=master)](https://travis-ci.org/brightcove/videojs-overlay) 4 | [![Greenkeeper badge](https://badges.greenkeeper.io/brightcove/videojs-overlay.svg)](https://greenkeeper.io/) 5 | [![Slack Status](http://slack.videojs.com/badge.svg)](http://slack.videojs.com) 6 | 7 | [![NPM](https://nodei.co/npm/videojs-overlay.png?downloads=true&downloadRank=true)](https://nodei.co/npm/videojs-overlay/) 8 | 9 | A plugin to display simple overlays - similar to YouTube's "Annotations" feature in appearance - during video playback. 10 | 11 | _Note_: This meaning of an "overlay" is distinct from that of a modal dialog, which can overlay the entire player. This is built into video.js as [the `ModalDialog` component](http://docs.videojs.com/docs/api/modal-dialog.html). 12 | 13 | Maintenance Status: Stable 14 | 15 | 16 | 17 | 18 | 19 | - [Getting Started](#getting-started) 20 | - [Documentation](#documentation) 21 | - [API](#api) 22 | - [`player.overlay()`](#playeroverlay) 23 | - [`overlay.get()`](#overlayget) 24 | - [`overlay.add(object|array)`](#overlayaddobjectarray) 25 | - [`overlay.remove(object)`](#overlayremoveobject) 26 | - [Plugin Options](#plugin-options) 27 | - [`align`](#align) 28 | - [`showBackground`](#showbackground) 29 | - [`attachToControlBar`](#attachtocontrolbar) 30 | - [`class`](#class) 31 | - [`content`](#content) 32 | - [`overlays`](#overlays) 33 | - [Examples](#examples) 34 | 35 | 36 | 37 | 38 | ## Getting Started 39 | 40 | Once you've added the plugin script to your page, you can use it with any video: 41 | 42 | ```html 43 | 44 | 47 | ``` 48 | 49 | There's also a [working example](https://github.com/brightcove/videojs-overlay/blob/master/index.html) of the plugin you can check out if you're having trouble. 50 | 51 | ## Documentation 52 | 53 | ### API 54 | #### `player.overlay()` 55 | This is the main interface and the way to initialize this plugin. It takes [an options object as input](#plugin-options). 56 | 57 | #### `overlay.get()` 58 | 59 | Returns an array of all the overlays set up for the current video. 60 | 61 | #### `overlay.add(Object|Array)` 62 | 63 | Adds one or more overlays to the current list of overlays without replacing the current list of overlays. 64 | Returns a reference to the added overlays. 65 | 66 | ```js 67 | const overlay = player.overlay({ 68 | content: 'Default overlay content', 69 | debug: true, 70 | overlays: [{ 71 | content: 'The video is playing!', 72 | start: 'play', 73 | end: 'pause' 74 | }] 75 | }); 76 | const addedOverlays = overlay.add({content: "this is a new one", start: "play", end: "pause"}); 77 | ``` 78 | 79 | 80 | #### `overlay.remove(Object)` 81 | 82 | Removes an individual overlay from the list of overlays. Calling this method with an invalid overlay object removes nothing from the list. 83 | 84 | ```js 85 | const overlay = player.overlay({ 86 | content: 'Default overlay content', 87 | debug: true, 88 | overlays: [{ 89 | content: 'The video is playing!', 90 | start: 'play', 91 | end: 'pause' 92 | }] 93 | }); 94 | const overlayToRemove = overlay.get()[0]; 95 | overlay.remove(overlayToRemove); 96 | ``` 97 | 98 | #### `overlay.reset(Object)` 99 | 100 | Once the plugin is initialized, the plugin options can be reset by passing this function an object of options. This will remove the previous configuration and overlays, and update the plugin with the new values. It takes [an options object as input](#plugin-options). 101 | 102 | ```js 103 | // First initialization 104 | const overlay = player.overlay({ 105 | debug: true, 106 | overlays: [{ 107 | content: 'The video is playing!', 108 | start: 'play', 109 | end: 'pause' 110 | }] 111 | }); 112 | 113 | // Update configuration with different overlays 114 | const overlayToRemove = overlay.reset({ 115 | debug: false, 116 | overlays: [{ 117 | content: 'Some new overlay content!', 118 | start: 'play', 119 | end: 'pause' 120 | }] 121 | }); 122 | ``` 123 | 124 | ### Plugin Options 125 | 126 | You may pass in an options object to the plugin upon initialization. This 127 | object may contain any of the following properties: 128 | 129 | #### `align` 130 | 131 | __Type:__ `String` 132 | __Default:__ `"top-left"` 133 | 134 | _This setting can be overridden by being set on individual overlay objects._ 135 | 136 | Where to display overlays, by default. Assuming the included stylesheet is used, the following values are supported: `"top-left"`, `"top"`, `"top-right"`, `"right"`, `"bottom-right"`, `"bottom"`, `"bottom-left"`, `"left"`. 137 | 138 | #### `showBackground` 139 | 140 | __Type:__ `Boolean` 141 | __Default:__ `true` 142 | 143 | _This setting can be overridden by being set on individual overlay objects._ 144 | 145 | Whether or not to include background styling & padding around the overlay. 146 | 147 | #### `attachToControlBar` 148 | 149 | __Type:__ `Boolean`, `String` 150 | __Default:__ `false` 151 | 152 | _This setting can be overridden by being set on individual overlay objects._ 153 | 154 | If set to `true` or a `string` value, bottom aligned overlays will adjust positioning when the control bar minimizes. This has no effect on overlays that are not aligned to bottom, bottom-left, or bottom-right. For use with the default control bar, it may not work for custom control bars. 155 | 156 | The value of `string` must be the name of a ControlBar component. 157 | 158 | Bottom aligned overlays will be inserted before the specified component. Otherwise, bottom aligned overlays are inserted before the first child component of the ControlBar. All other overlays are inserted before the ControlBar component. 159 | 160 | #### `class` 161 | 162 | __Type:__ `String` 163 | __Default:__ `""` 164 | 165 | _This setting can be overridden by being set on individual overlay objects._ 166 | 167 | A custom HTML class to add to each overlay element. 168 | 169 | #### `content` 170 | 171 | __Type:__ `String`, `Element`, `DocumentFragment` 172 | __Default:__ `"This overlay will show up while the video is playing"` 173 | 174 | _This setting can be overridden by being set on individual overlay objects._ 175 | 176 | The default HTML that the overlay includes. 177 | 178 | #### `overlays` 179 | 180 | __Type:__ `Array` 181 | __Default:__ an array with a single example overlay 182 | 183 | An array of overlay objects. An overlay object should consist of: 184 | 185 | - `start` (`String` or `Number`): When to show the overlay. If its value is a string, it is understood as the name of an event. If it is a number, the overlay will be shown when that moment in the playback timeline is passed. 186 | - `end` (`String` or `Number`): When to hide the overlay. The values of this property have the same semantics as `start`. 187 | 188 | And it can optionally include `align`, `class`, and/or `content` to override top-level settings. 189 | 190 | All properties are currently optional. That is, you may leave `start` or `end` off and the plugin will not complain, but you should always pass a `start` and an `end`. This will be required in a future release. 191 | 192 | ### Examples 193 | 194 | You can setup overlays to be displayed when particular events are emitted by the player, including your own custom events: 195 | 196 | ```js 197 | player.overlay({ 198 | overlays: [{ 199 | 200 | // This overlay will appear when a video is playing and disappear when 201 | // the player is paused. 202 | start: 'playing', 203 | end: 'pause' 204 | }, { 205 | 206 | // This overlay will appear when the "custom1" event is triggered and 207 | // disappear when the "custom2" event is triggered. 208 | start: 'custom1', 209 | end: 'custom2' 210 | }] 211 | }); 212 | ``` 213 | 214 | Multiple overlays can be displayed simultaneously. You probably want to specify an alignment for one or more of them so they don't overlap: 215 | 216 | ```js 217 | player.overlay({ 218 | overlays: [{ 219 | 220 | // This overlay appears at 3 seconds and disappears at 15 seconds. 221 | start: 3, 222 | end: 15 223 | }, { 224 | 225 | // This overlay appears at 7 seconds and disappears at 22 seconds. 226 | start: 7, 227 | end: 22, 228 | align: 'bottom' 229 | }] 230 | }); 231 | ``` 232 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | videojs-overlay Demo 6 | 7 | 8 | 9 | 10 | 14 | 17 | 18 | 19 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "videojs-overlay", 3 | "description": "A plugin to display simple overlays during video playback.", 4 | "author": "Brightcove, Inc.", 5 | "license": "Apache-2.0", 6 | "version": "4.0.0", 7 | "main": "dist/videojs-overlay.cjs.js", 8 | "module": "dist/videojs-overlay.es.js", 9 | "keywords": [ 10 | "videojs", 11 | "videojs-plugin" 12 | ], 13 | "repository": "https://github.com/brightcove/videojs-overlay.git", 14 | "vjsstandard": { 15 | "ignore": [ 16 | "dist", 17 | "docs", 18 | "test/dist" 19 | ] 20 | }, 21 | "scripts": { 22 | "prebuild": "npm run clean", 23 | "build": "npm-run-all -p build:*", 24 | "build:css": "sass --load-path=node_modules src/plugin.scss dist/videojs-overlay.css --style compressed", 25 | "build:js": "rollup -c scripts/rollup.config.js", 26 | "clean": "shx rm -rf ./dist ./test/dist", 27 | "postclean": "shx mkdir -p ./dist ./test/dist", 28 | "docs": "doctoc README.md", 29 | "lint": "vjsstandard", 30 | "server": "karma start scripts/karma.conf.js --singleRun=false --auto-watch", 31 | "start": "npm-run-all -p server watch", 32 | "pretest": "npm-run-all lint build", 33 | "test": "karma start scripts/karma.conf.js", 34 | "update-changelog": "conventional-changelog -p videojs -i CHANGELOG.md -s", 35 | "preversion": "npm test", 36 | "version": "is-prerelease || npm run update-changelog && git add CHANGELOG.md", 37 | "watch": "npm-run-all -p watch:*", 38 | "watch:css": "npm run build:css -- -w", 39 | "watch:js": "npm run build:js -- -w", 40 | "posttest": "shx cat test/dist/coverage/text.txt", 41 | "prepublishOnly": "npm run build && vjsverify --skip-es-check" 42 | }, 43 | "dependencies": { 44 | "global": "^4.3.2", 45 | "video.js": "^6 || ^7 || ^8" 46 | }, 47 | "devDependencies": { 48 | "conventional-changelog-cli": "^2.0.1", 49 | "conventional-changelog-videojs": "^3.0.0", 50 | "doctoc": "^1.3.1", 51 | "husky": "^1.0.0-rc.13", 52 | "karma": "^3.0.0", 53 | "lint-staged": "^8.1.0", 54 | "not-prerelease": "^1.0.1", 55 | "npm-merge-driver-install": "^1.0.0", 56 | "npm-run-all": "^4.1.5", 57 | "pkg-ok": "^2.2.0", 58 | "postcss-cli": "^6.0.0", 59 | "rollup": "^2.61.1", 60 | "sass": "^1.79.5", 61 | "shx": "^0.3.2", 62 | "sinon": "^6.1.5", 63 | "videojs-generate-karma-config": "^8.0.1", 64 | "videojs-generate-postcss-config": "~2.0.1", 65 | "videojs-generate-rollup-config": "^7.0.0", 66 | "videojs-generator-verify": "^4.0.1", 67 | "videojs-standard": "^9.0.1" 68 | }, 69 | "style": "dist/videojs-overlay.css", 70 | "videojs-plugin": { 71 | "style": "dist/videojs-overlay.css", 72 | "script": "dist/videojs-overlay.min.js" 73 | }, 74 | "files": [ 75 | "CONTRIBUTING.md", 76 | "dist/", 77 | "docs/", 78 | "index.html", 79 | "scripts/", 80 | "src/", 81 | "test/" 82 | ], 83 | "exports": { 84 | ".": { 85 | "require": "./dist/videojs-overlay.cjs.js", 86 | "import": "./dist/videojs-overlay.es.js" 87 | }, 88 | "./plugin-only": { 89 | "require": "./dist/videojs-overlay.plugin.js", 90 | "import": "./dist/videojs-overlay.plugin.js" 91 | } 92 | }, 93 | "generator-videojs-plugin": { 94 | "version": "7.3.2" 95 | }, 96 | "lint-staged": { 97 | "*.js": [ 98 | "vjsstandard --fix", 99 | "git add" 100 | ], 101 | "README.md": [ 102 | "git add" 103 | ] 104 | }, 105 | "husky": { 106 | "hooks": { 107 | "pre-commit": "lint-staged" 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /scripts/karma.conf.js: -------------------------------------------------------------------------------- 1 | const generate = require('videojs-generate-karma-config'); 2 | 3 | module.exports = function(config) { 4 | 5 | // see https://github.com/videojs/videojs-generate-karma-config 6 | // for options 7 | const options = {}; 8 | 9 | config = generate(config, options); 10 | 11 | // any other custom stuff not supported by options here! 12 | }; 13 | 14 | -------------------------------------------------------------------------------- /scripts/postcss.config.js: -------------------------------------------------------------------------------- 1 | const generate = require('videojs-generate-postcss-config'); 2 | 3 | module.exports = function(context) { 4 | const result = generate({}, context); 5 | 6 | // do custom stuff here 7 | 8 | return result; 9 | }; 10 | -------------------------------------------------------------------------------- /scripts/rollup.config.js: -------------------------------------------------------------------------------- 1 | const generate = require('videojs-generate-rollup-config'); 2 | 3 | // see https://github.com/videojs/videojs-generate-rollup-config 4 | // for options 5 | const options = { 6 | input: 'src/index.js' 7 | }; 8 | 9 | // Generate the original for plugin 10 | const indexConfig = generate(options); 11 | 12 | // Config for file that exports plugin without registering it 13 | const pluginOnlyConfig = { 14 | watch: { clearScreen: false }, 15 | input: 'src/plugin.js', 16 | external: indexConfig.builds.module.external, 17 | output: [ 18 | { 19 | file: './dist/videojs-overlay.plugin.js', 20 | format: 'umd', 21 | name: indexConfig.settings.exportName, 22 | banner: indexConfig.settings.banner, 23 | globals: { 'video.js': 'videojs', 'global/window': 'window' } 24 | } 25 | ], 26 | plugins: indexConfig.plugins 27 | }; 28 | 29 | // Add additonal builds/customization here! 30 | 31 | const configs = Object.values(indexConfig.builds); 32 | 33 | configs.push(pluginOnlyConfig); 34 | 35 | // export the builds to rollup 36 | export default configs; 37 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import videojs from 'video.js'; 2 | import initOverlayComponent from './overlay-component'; 3 | import OverlayPlugin from './plugin'; 4 | import {version as VERSION} from '../package.json'; 5 | 6 | initOverlayComponent(videojs); 7 | 8 | OverlayPlugin.VERSION = VERSION; 9 | 10 | videojs.registerPlugin('overlay', OverlayPlugin); 11 | 12 | export default OverlayPlugin; 13 | -------------------------------------------------------------------------------- /src/overlay-component.js: -------------------------------------------------------------------------------- 1 | import window from 'global/window'; 2 | 3 | const initOverlayComponent = (videojs) => { 4 | const Component = videojs.getComponent('Component'); 5 | 6 | const dom = videojs.dom || videojs; 7 | 8 | /** 9 | * Whether the value is a `Number`. 10 | * 11 | * Both `Infinity` and `-Infinity` are accepted, but `NaN` is not. 12 | * 13 | * @param {Number} n 14 | * @return {Boolean} 15 | */ 16 | 17 | /* eslint-disable no-self-compare */ 18 | const isNumber = n => typeof n === 'number' && n === n; 19 | /* eslint-enable no-self-compare */ 20 | 21 | /** 22 | * Whether a value is a string with no whitespace. 23 | * 24 | * @param {string} s 25 | * @return {boolean} 26 | */ 27 | const hasNoWhitespace = s => typeof s === 'string' && (/^\S+$/).test(s); 28 | 29 | /** 30 | * Overlay component. 31 | * 32 | * @class Overlay 33 | * @extends {videojs.Component} 34 | */ 35 | class Overlay extends Component { 36 | 37 | constructor(player, options) { 38 | super(player, options); 39 | 40 | ['start', 'end'].forEach(key => { 41 | const value = this.options_[key]; 42 | 43 | if (isNumber(value)) { 44 | this[key + 'Event_'] = 'timeupdate'; 45 | } else if (hasNoWhitespace(value)) { 46 | this[key + 'Event_'] = value; 47 | 48 | // An overlay MUST have a start option. Otherwise, it's pointless. 49 | } else if (key === 'start') { 50 | throw new Error('invalid "start" option; expected number or string'); 51 | } 52 | }); 53 | 54 | // video.js does not like components with multiple instances binding 55 | // events to the player because it tracks them at the player level, 56 | // not at the level of the object doing the binding. This could also be 57 | // solved with Function.prototype.bind (but not videojs.bind because of 58 | // its GUID magic), but the anonymous function approach avoids any issues 59 | // caused by crappy libraries clobbering Function.prototype.bind. 60 | // - https://github.com/videojs/video.js/issues/3097 61 | ['endListener_', 'rewindListener_', 'startListener_'].forEach(name => { 62 | this[name] = (e) => Overlay.prototype[name].call(this, e); 63 | }); 64 | 65 | // If the start event is a timeupdate, we need to watch for rewinds (i.e., 66 | // when the user seeks backward). 67 | if (this.startEvent_ === 'timeupdate') { 68 | this.on(player, 'timeupdate', this.rewindListener_); 69 | } 70 | 71 | this.debug(`created, listening to "${this.startEvent_}" for "start" and "${this.endEvent_ || 'nothing'}" for "end"`); 72 | 73 | this.hide(); 74 | } 75 | 76 | createEl() { 77 | const options = this.options_; 78 | const content = options.content; 79 | 80 | const background = options.showBackground ? 'vjs-overlay-background' : 'vjs-overlay-no-background'; 81 | const el = dom.createEl('div', { 82 | className: ` 83 | vjs-overlay 84 | vjs-overlay-${options.align} 85 | ${options.class} 86 | ${background} 87 | vjs-hidden 88 | ` 89 | }); 90 | 91 | if (typeof content === 'string') { 92 | el.innerHTML = content; 93 | } else if (content instanceof window.DocumentFragment) { 94 | el.appendChild(content); 95 | } else { 96 | dom.appendContent(el, content); 97 | } 98 | 99 | return el; 100 | } 101 | 102 | /** 103 | * Logs debug errors 104 | * 105 | * @param {...[type]} args [description] 106 | * @return {[type]} [description] 107 | */ 108 | debug(...args) { 109 | if (!this.options_.debug) { 110 | return; 111 | } 112 | 113 | const log = videojs.log; 114 | let fn = log; 115 | 116 | // Support `videojs.log.foo` calls. 117 | if (log.hasOwnProperty(args[0]) && typeof log[args[0]] === 'function') { 118 | fn = log[args.shift()]; 119 | } 120 | 121 | fn(...[`overlay#${this.id()}: `, ...args]); 122 | } 123 | 124 | /** 125 | * Overrides the inherited method to perform some event binding 126 | * 127 | * @return {Overlay} 128 | */ 129 | hide() { 130 | super.hide(); 131 | 132 | this.debug('hidden'); 133 | this.debug(`bound \`startListener_\` to "${this.startEvent_}"`); 134 | 135 | // Overlays without an "end" are valid. 136 | if (this.endEvent_) { 137 | this.debug(`unbound \`endListener_\` from "${this.endEvent_}"`); 138 | this.off(this.player(), this.endEvent_, this.endListener_); 139 | } 140 | 141 | this.on(this.player(), this.startEvent_, this.startListener_); 142 | 143 | return this; 144 | } 145 | 146 | /** 147 | * Determine whether or not the overlay should hide. 148 | * 149 | * @param {number} time 150 | * The current time reported by the player. 151 | * @param {string} type 152 | * An event type. 153 | * @return {boolean} 154 | */ 155 | shouldHide_(time, type) { 156 | const end = this.options_.end; 157 | 158 | return isNumber(end) ? (time >= end) : end === type; 159 | } 160 | 161 | /** 162 | * Overrides the inherited method to perform some event binding 163 | * 164 | * @return {Overlay} 165 | */ 166 | show() { 167 | super.show(); 168 | this.off(this.player(), this.startEvent_, this.startListener_); 169 | this.debug('shown'); 170 | this.debug(`unbound \`startListener_\` from "${this.startEvent_}"`); 171 | 172 | // Overlays without an "end" are valid. 173 | if (this.endEvent_) { 174 | this.debug(`bound \`endListener_\` to "${this.endEvent_}"`); 175 | this.on(this.player(), this.endEvent_, this.endListener_); 176 | } 177 | 178 | return this; 179 | } 180 | 181 | /** 182 | * Determine whether or not the overlay should show. 183 | * 184 | * @param {number} time 185 | * The current time reported by the player. 186 | * @param {string} type 187 | * An event type. 188 | * @return {boolean} 189 | */ 190 | shouldShow_(time, type) { 191 | const start = this.options_.start; 192 | const end = this.options_.end; 193 | 194 | if (isNumber(start)) { 195 | 196 | if (isNumber(end)) { 197 | return time >= start && time < end; 198 | 199 | // In this case, the start is a number and the end is a string. We need 200 | // to check whether or not the overlay has shown since the last seek. 201 | } else if (!this.hasShownSinceSeek_) { 202 | this.hasShownSinceSeek_ = true; 203 | return time >= start; 204 | } 205 | 206 | // In this case, the start is a number and the end is a string, but 207 | // the overlay has shown since the last seek. This means that we need 208 | // to be sure we aren't re-showing it at a later time than it is 209 | // scheduled to appear. 210 | return Math.floor(time) === start; 211 | } 212 | 213 | return start === type; 214 | } 215 | 216 | /** 217 | * Event listener that can trigger the overlay to show. 218 | * 219 | * @param {Event} e 220 | */ 221 | startListener_(e) { 222 | const time = this.player().currentTime(); 223 | 224 | if (this.shouldShow_(time, e.type)) { 225 | this.show(); 226 | } 227 | } 228 | 229 | /** 230 | * Event listener that can trigger the overlay to show. 231 | * 232 | * @param {Event} e 233 | */ 234 | endListener_(e) { 235 | const time = this.player().currentTime(); 236 | 237 | if (this.shouldHide_(time, e.type)) { 238 | this.hide(); 239 | } 240 | } 241 | 242 | /** 243 | * Event listener that can looks for rewinds - that is, backward seeks 244 | * and may hide the overlay as needed. 245 | * 246 | * @param {Event} e 247 | */ 248 | rewindListener_(e) { 249 | const time = this.player().currentTime(); 250 | const previous = this.previousTime_; 251 | const start = this.options_.start; 252 | const end = this.options_.end; 253 | 254 | // Did we seek backward? 255 | if (time < previous) { 256 | this.debug('rewind detected'); 257 | 258 | // The overlay remains visible if two conditions are met: the end value 259 | // MUST be an integer and the the current time indicates that the 260 | // overlay should NOT be visible. 261 | if (isNumber(end) && !this.shouldShow_(time)) { 262 | this.debug(`hiding; ${end} is an integer and overlay should not show at this time`); 263 | this.hasShownSinceSeek_ = false; 264 | this.hide(); 265 | 266 | // If the end value is an event name, we cannot reliably decide if the 267 | // overlay should still be displayed based solely on time; so, we can 268 | // only queue it up for showing if the seek took us to a point before 269 | // the start time. 270 | } else if (hasNoWhitespace(end) && time < start) { 271 | this.debug(`hiding; show point (${start}) is before now (${time}) and end point (${end}) is an event`); 272 | this.hasShownSinceSeek_ = false; 273 | this.hide(); 274 | } 275 | } 276 | 277 | this.previousTime_ = time; 278 | } 279 | } 280 | 281 | videojs.registerComponent('Overlay', Overlay); 282 | 283 | return Overlay; 284 | }; 285 | 286 | export default initOverlayComponent; 287 | -------------------------------------------------------------------------------- /src/plugin.js: -------------------------------------------------------------------------------- 1 | import videojs from 'video.js'; 2 | import initOverlayComponent from './overlay-component'; 3 | 4 | const Plugin = videojs.getPlugin('plugin'); 5 | 6 | const defaults = { 7 | align: 'top-left', 8 | class: '', 9 | content: 'This overlay will show up while the video is playing', 10 | debug: false, 11 | showBackground: true, 12 | attachToControlBar: false, 13 | overlays: [{ 14 | start: 'playing', 15 | end: 'paused' 16 | }] 17 | }; 18 | 19 | /** 20 | * A plugin for handling overlays in the Brightcove Player. 21 | */ 22 | class OverlayPlugin extends Plugin { 23 | /** 24 | * Create an Overlay Plugin instance. 25 | * 26 | * @param {Player} player 27 | * A Video.js Player instance. 28 | * 29 | * @param {Object} [options] 30 | * An options object. 31 | */ 32 | constructor(player, options) { 33 | super(player); 34 | 35 | this.reset(options); 36 | } 37 | 38 | /** 39 | * Adds one or more items to the existing list of overlays. 40 | * 41 | * @param {Object|Array} item 42 | * An item (or an array of items) to be added as overlay/s 43 | * 44 | * @return {Array[Overlay]} 45 | * The array of overlay objects that were added 46 | */ 47 | add(item) { 48 | if (!Array.isArray(item)) { 49 | item = [item]; 50 | } 51 | 52 | const addedOverlays = this.mapOverlays_(item); 53 | 54 | this.player.overlays_ = this.player.overlays_.concat(addedOverlays); 55 | 56 | return addedOverlays; 57 | } 58 | 59 | /** 60 | * 61 | * @param {Overlay} item 62 | * An item to be removed from the array of overlays 63 | * 64 | * @throws {Error} 65 | * Item to remove must be present in the array of overlays 66 | * 67 | */ 68 | remove(item) { 69 | const index = this.player.overlays_.indexOf(item); 70 | 71 | if (index !== -1) { 72 | item.el().parentNode.removeChild(item.el()); 73 | this.player.overlays_.splice(index, 1); 74 | } else { 75 | this.player.log.warn('overlay does not exist and cannot be removed'); 76 | } 77 | } 78 | 79 | /** 80 | * Gets the array of overlays used for the current video 81 | * 82 | * @return The array of overlay objects currently used by the plugin 83 | */ 84 | get() { 85 | return this.player.overlays_; 86 | } 87 | 88 | /** 89 | * Updates the overlay options 90 | * 91 | * @param {Object} [options] 92 | * An options object. 93 | */ 94 | reset(options) { 95 | this.clearOverlays_(); 96 | 97 | // Use merge function based on video.js version. 98 | const merge = videojs.obj && videojs.obj.merge || videojs.mergeOptions; 99 | 100 | this.options = merge(defaults, options); 101 | 102 | const overlays = this.options.overlays; 103 | 104 | // We don't want to keep the original array of overlay options around 105 | // because it doesn't make sense to pass it to each Overlay component. 106 | delete this.options.overlays; 107 | 108 | this.player.overlays_ = this.mapOverlays_(overlays); 109 | } 110 | 111 | /** 112 | * Disposes the plugin 113 | */ 114 | dispose() { 115 | this.clearOverlays_(); 116 | 117 | delete this.player.overlays_; 118 | super.dispose(); 119 | } 120 | 121 | clearOverlays_() { 122 | // Remove child components 123 | if (Array.isArray(this.player.overlays_)) { 124 | this.player.overlays_.forEach(overlay => { 125 | this.player.removeChild(overlay); 126 | if (this.player.controlBar) { 127 | this.player.controlBar.removeChild(overlay); 128 | } 129 | }); 130 | } 131 | } 132 | 133 | mapOverlays_(items) { 134 | return items.map(o => { 135 | const mergeOptions = videojs.mergeOptions(this.options, o); 136 | const attachToControlBar = typeof mergeOptions.attachToControlBar === 'string' || mergeOptions.attachToControlBar === true; 137 | 138 | if (!this.player.controls() || !this.player.controlBar) { 139 | return this.player.addChild('overlay', mergeOptions); 140 | } 141 | 142 | if (attachToControlBar && mergeOptions.align.indexOf('bottom') !== -1) { 143 | let referenceChild = this.player.controlBar.children()[0]; 144 | 145 | if (this.player.controlBar.getChild(mergeOptions.attachToControlBar) !== undefined) { 146 | referenceChild = this.player.controlBar.getChild(mergeOptions.attachToControlBar); 147 | } 148 | 149 | if (referenceChild) { 150 | const referenceChildIndex = this.player.controlBar.children().indexOf(referenceChild); 151 | const controlBarChild = this.player.controlBar.addChild('overlay', mergeOptions, referenceChildIndex); 152 | 153 | return controlBarChild; 154 | } 155 | } 156 | 157 | const playerChild = this.player.addChild('overlay', mergeOptions); 158 | 159 | this.player.el().insertBefore( 160 | playerChild.el(), 161 | this.player.controlBar.el() 162 | ); 163 | 164 | return playerChild; 165 | }); 166 | } 167 | } 168 | 169 | export { initOverlayComponent }; 170 | 171 | export default OverlayPlugin; 172 | -------------------------------------------------------------------------------- /src/plugin.scss: -------------------------------------------------------------------------------- 1 | .video-js { 2 | $bottom: 3.5em; 3 | $nudge: 5px; 4 | $middle: 50%; 5 | $offset-h: -16.5%; 6 | $offset-v: -15px; 7 | 8 | .vjs-overlay { 9 | color: #fff; 10 | position: absolute; 11 | text-align: center; 12 | } 13 | 14 | .vjs-overlay-no-background { 15 | max-width: 33%; 16 | } 17 | 18 | .vjs-overlay-background { 19 | // IE8 20 | background-color: #646464; 21 | background-color: rgba(255, 255, 255, 0.4); 22 | border-radius: round($nudge / 2); 23 | padding: $nudge * 2; 24 | width: 33%; 25 | } 26 | 27 | .vjs-overlay-top-left { 28 | top: $nudge; 29 | left: $nudge; 30 | } 31 | 32 | .vjs-overlay-top { 33 | left: $middle; 34 | margin-left: $offset-h; 35 | top: $nudge; 36 | } 37 | 38 | .vjs-overlay-top-right { 39 | right: $nudge; 40 | top: $nudge; 41 | } 42 | 43 | .vjs-overlay-right { 44 | right: $nudge; 45 | top: $middle; 46 | transform: translateY(-50%); 47 | } 48 | 49 | .vjs-overlay-bottom-right { 50 | bottom: $bottom; 51 | right: $nudge; 52 | } 53 | 54 | .vjs-overlay-bottom { 55 | bottom: $bottom; 56 | left: $middle; 57 | margin-left: $offset-h; 58 | } 59 | 60 | .vjs-overlay-bottom-left { 61 | bottom: $bottom; 62 | left: $nudge; 63 | } 64 | 65 | .vjs-overlay-left { 66 | left: $nudge; 67 | top: $middle; 68 | transform: translateY(-50%); 69 | } 70 | 71 | .vjs-overlay-center { 72 | left: $middle; 73 | margin-left: $offset-h; 74 | top: $middle; 75 | transform: translateY(-50%); 76 | } 77 | 78 | // Fallback for IE8 and IE9 79 | .vjs-no-flex .vjs-overlay-left, 80 | .vjs-no-flex .vjs-overlay-center, 81 | .vjs-no-flex .vjs-overlay-right { 82 | margin-top: $offset-v; 83 | } 84 | } 85 | 86 | -------------------------------------------------------------------------------- /test/plugin.test.js: -------------------------------------------------------------------------------- 1 | import document from 'global/document'; 2 | 3 | import QUnit from 'qunit'; 4 | import sinon from 'sinon'; 5 | import videojs from 'video.js'; 6 | 7 | import plugin from '../src/index'; 8 | 9 | const Player = videojs.getComponent('Player'); 10 | const dom = videojs.dom || videojs; 11 | 12 | QUnit.test('the environment is sane', function(assert) { 13 | assert.strictEqual(typeof Array.isArray, 'function', 'es5 exists'); 14 | assert.strictEqual(typeof sinon, 'object', 'sinon exists'); 15 | assert.strictEqual(typeof videojs, 'function', 'videojs exists'); 16 | assert.strictEqual(typeof plugin, 'function', 'plugin is a function'); 17 | }); 18 | 19 | QUnit.module('videojs-overlay', { 20 | 21 | beforeEach() { 22 | 23 | // Mock the environment's timers because certain things - particularly 24 | // player readiness - are asynchronous in video.js 5. This MUST come 25 | // before any player is created; otherwise, timers could get created 26 | // with the actual timer methods! 27 | this.clock = sinon.useFakeTimers(); 28 | 29 | this.fixture = document.getElementById('qunit-fixture'); 30 | this.video = document.createElement('video'); 31 | this.video.controls = true; 32 | this.fixture.appendChild(this.video); 33 | this.player = videojs(this.video); 34 | 35 | // Simulate the video element playing to a specific time and stub 36 | // the `currentTime` method of the player to return this. 37 | this.currentTime = 0; 38 | 39 | this.player.currentTime = () => this.currentTime; 40 | 41 | this.updateTime = seconds => { 42 | this.currentTime = seconds; 43 | this.player.trigger('timeupdate'); 44 | }; 45 | 46 | this.assertOverlayCount = (assert, expected) => { 47 | const overlays = Array.prototype.filter.call( 48 | this.player.$$('.vjs-overlay'), 49 | el => !dom.hasClass(el, 'vjs-hidden') 50 | ); 51 | const actual = overlays ? overlays.length : 0; 52 | const one = expected === 1; 53 | const msg = `${expected} overlay${one ? '' : 's'} exist${one ? 's' : ''}`; 54 | 55 | assert.strictEqual(actual, expected, msg); 56 | }; 57 | }, 58 | 59 | afterEach() { 60 | this.player.dispose(); 61 | this.clock.restore(); 62 | } 63 | }); 64 | 65 | QUnit.test('registers itself with video.js', function(assert) { 66 | assert.expect(2); 67 | 68 | assert.strictEqual( 69 | typeof Player.prototype.overlay, 70 | 'function', 71 | 'videojs-overlay plugin was registered' 72 | ); 73 | 74 | assert.ok(videojs.getComponent('Overlay'), 'the Overlay component was registered'); 75 | }); 76 | 77 | QUnit.test('does not display overlays when none are configured', function(assert) { 78 | assert.expect(1); 79 | 80 | this.player.overlay({ 81 | overlays: [] 82 | }); 83 | 84 | this.assertOverlayCount(assert, 0); 85 | }); 86 | 87 | QUnit.test('can be triggered and dismissed by events', function(assert) { 88 | assert.expect(3); 89 | 90 | this.player.overlay({ 91 | overlays: [{ 92 | start: 'custom-start', 93 | end: 'custom-end' 94 | }] 95 | }); 96 | 97 | this.assertOverlayCount(assert, 0); 98 | 99 | this.player.trigger('custom-start'); 100 | this.assertOverlayCount(assert, 1); 101 | 102 | this.player.trigger('custom-end'); 103 | this.assertOverlayCount(assert, 0); 104 | }); 105 | 106 | QUnit.test('can be triggered for time intervals', function(assert) { 107 | assert.expect(7); 108 | 109 | this.player.overlay({ 110 | overlays: [{ 111 | start: 5, 112 | end: 10 113 | }] 114 | }); 115 | 116 | this.updateTime(4); 117 | this.assertOverlayCount(assert, 0); 118 | 119 | this.updateTime(5); 120 | this.assertOverlayCount(assert, 1); 121 | 122 | this.updateTime(7.5); 123 | this.assertOverlayCount(assert, 1); 124 | 125 | this.updateTime(10); 126 | this.assertOverlayCount(assert, 0); 127 | 128 | this.updateTime(11); 129 | this.assertOverlayCount(assert, 0); 130 | 131 | this.updateTime(6); 132 | this.assertOverlayCount(assert, 1); 133 | 134 | this.updateTime(12); 135 | this.assertOverlayCount(assert, 0); 136 | }); 137 | 138 | QUnit.test('shows multiple overlays simultaneously', function(assert) { 139 | assert.expect(4); 140 | 141 | this.player.overlay({ 142 | overlays: [{ 143 | start: 3, 144 | end: 10 145 | }, { 146 | start: 'playing', 147 | end: 'ended' 148 | }] 149 | }); 150 | 151 | this.updateTime(4); 152 | this.assertOverlayCount(assert, 1); 153 | 154 | this.player.trigger('playing'); 155 | this.assertOverlayCount(assert, 2); 156 | 157 | this.player.trigger('ended'); 158 | this.assertOverlayCount(assert, 1); 159 | 160 | this.updateTime(11); 161 | this.assertOverlayCount(assert, 0); 162 | }); 163 | 164 | QUnit.test( 165 | 'the content of overlays can be specified as an HTML string', 166 | function(assert) { 167 | assert.expect(1); 168 | 169 | const innerHTML = '

overlay text

'; 170 | 171 | this.player.overlay({ 172 | content: innerHTML, 173 | overlays: [{ 174 | start: 'playing', 175 | end: 'ended' 176 | }] 177 | }); 178 | 179 | this.player.trigger('playing'); 180 | 181 | assert.strictEqual( 182 | this.player.$('.vjs-overlay').innerHTML, 183 | innerHTML, 184 | 'innerHTML matched' 185 | ); 186 | } 187 | ); 188 | 189 | QUnit.test('an element can be used as the content of overlays', function(assert) { 190 | assert.expect(1); 191 | 192 | const content = document.createElement('p'); 193 | 194 | content.innerHTML = 'this is some text'; 195 | 196 | this.player.overlay({ 197 | content, 198 | overlays: [{ 199 | start: 5, 200 | end: 10 201 | }] 202 | }); 203 | 204 | this.updateTime(5); 205 | 206 | assert.strictEqual( 207 | this.player.$('.vjs-overlay p'), 208 | content, 209 | 'sets the content element' 210 | ); 211 | }); 212 | 213 | QUnit.test('a DocumentFragment can be used as the content of overlays', function(assert) { 214 | assert.expect(1); 215 | 216 | const fragment = document.createDocumentFragment(); 217 | const br = document.createElement('br'); 218 | 219 | fragment.appendChild(br); 220 | 221 | this.player.overlay({ 222 | content: fragment, 223 | overlays: [{ 224 | start: 'showoverlay', 225 | end: 'hideoverlay' 226 | }] 227 | }); 228 | 229 | this.player.trigger('showoverlay'); 230 | 231 | assert.strictEqual( 232 | this.player.$('.vjs-overlay br'), 233 | br, 234 | 'sets the content fragment' 235 | ); 236 | }); 237 | 238 | QUnit.test('allows content to be specified per overlay', function(assert) { 239 | assert.expect(5); 240 | 241 | const text = 'some text'; 242 | const html = '

overlay text

'; 243 | const element = document.createElement('i'); 244 | const fragment = document.createDocumentFragment(); 245 | 246 | fragment.appendChild(document.createElement('img')); 247 | 248 | this.player.overlay({ 249 | content: text, 250 | overlays: [{ 251 | start: 0, 252 | end: 1 253 | }, { 254 | content: html, 255 | start: 0, 256 | end: 1 257 | }, { 258 | content: element, 259 | start: 0, 260 | end: 1 261 | }, { 262 | content: fragment, 263 | start: 0, 264 | end: 1 265 | }] 266 | }); 267 | 268 | this.updateTime(0); 269 | this.assertOverlayCount(assert, 4); 270 | 271 | assert.strictEqual( 272 | this.player.$$('.vjs-overlay b').length, 273 | 1, 274 | 'shows a default overlay' 275 | ); 276 | 277 | assert.strictEqual( 278 | this.player.$$('.vjs-overlay p').length, 279 | 1, 280 | 'shows an HTML string' 281 | ); 282 | 283 | assert.strictEqual( 284 | this.player.$$('.vjs-overlay i').length, 285 | 1, 286 | 'shows a DOM element' 287 | ); 288 | 289 | assert.strictEqual( 290 | this.player.$$('.vjs-overlay img').length, 291 | 1, 292 | 'shows a document fragment' 293 | ); 294 | }); 295 | 296 | QUnit.test('allows css class to be specified per overlay', function(assert) { 297 | assert.expect(3); 298 | 299 | const text = 'some text'; 300 | const fragment = document.createDocumentFragment(); 301 | 302 | fragment.appendChild(document.createElement('img')); 303 | 304 | this.player.overlay({ 305 | content: text, 306 | overlays: [{ 307 | class: 'first-class-overlay', 308 | start: 0, 309 | end: 1 310 | }, { 311 | class: 'second-class-overlay', 312 | start: 0, 313 | end: 1 314 | }, { 315 | start: 0, 316 | end: 1 317 | }] 318 | }); 319 | 320 | this.updateTime(0); 321 | 322 | this.assertOverlayCount(assert, 3); 323 | 324 | assert.strictEqual( 325 | this.player.$$('.first-class-overlay').length, 326 | 1, 327 | 'shows an overlay with a custom class' 328 | ); 329 | 330 | assert.strictEqual( 331 | this.player.$$('.second-class-overlay').length, 332 | 1, 333 | 'shows an overlay with a different custom class' 334 | ); 335 | }); 336 | 337 | QUnit.test('does not double add overlays that are triggered twice', function(assert) { 338 | assert.expect(1); 339 | 340 | this.player.overlay({ 341 | overlays: [{ 342 | start: 'start', 343 | end: 'end' 344 | }] 345 | }); 346 | 347 | this.player.trigger('start'); 348 | this.player.trigger('start'); 349 | this.assertOverlayCount(assert, 1); 350 | }); 351 | 352 | QUnit.test('does not double remove overlays that are triggered twice', function(assert) { 353 | assert.expect(1); 354 | 355 | this.player.overlay({ 356 | overlays: [{ 357 | start: 'start', 358 | end: 'end' 359 | }] 360 | }); 361 | 362 | this.player.trigger('start'); 363 | this.player.trigger('end'); 364 | this.player.trigger('end'); 365 | this.assertOverlayCount(assert, 0); 366 | }); 367 | 368 | QUnit.test( 369 | 'displays overlays that mix event and playback time triggers', 370 | function(assert) { 371 | assert.expect(4); 372 | 373 | this.player.overlay({ 374 | overlays: [{ 375 | start: 'start', 376 | end: 10 377 | }, { 378 | start: 5, 379 | end: 'end' 380 | }] 381 | }); 382 | 383 | this.player.trigger('start'); 384 | this.assertOverlayCount(assert, 1); 385 | 386 | this.updateTime(6); 387 | this.assertOverlayCount(assert, 2); 388 | 389 | this.updateTime(10); 390 | this.assertOverlayCount(assert, 1); 391 | 392 | this.player.trigger('end'); 393 | this.assertOverlayCount(assert, 0); 394 | } 395 | ); 396 | 397 | QUnit.test('shows mixed trigger overlays once per seek', function(assert) { 398 | assert.expect(6); 399 | 400 | this.player.overlay({ 401 | overlays: [{ 402 | start: 1, 403 | end: 'pause' 404 | }] 405 | }); 406 | 407 | this.updateTime(1); 408 | this.assertOverlayCount(assert, 1); 409 | 410 | this.player.trigger('pause'); 411 | this.assertOverlayCount(assert, 0); 412 | 413 | this.updateTime(2); 414 | this.assertOverlayCount(assert, 0); 415 | 416 | this.updateTime(1); 417 | this.assertOverlayCount(assert, 1); 418 | 419 | this.player.trigger('pause'); 420 | this.assertOverlayCount(assert, 0); 421 | 422 | this.updateTime(2); 423 | this.assertOverlayCount(assert, 0); 424 | }); 425 | 426 | QUnit.test('applies simple alignment class names', function(assert) { 427 | assert.expect(4); 428 | 429 | this.player.overlay({ 430 | overlays: [{ 431 | start: 'start', 432 | align: 'top' 433 | }, { 434 | start: 'start', 435 | align: 'left' 436 | }, { 437 | start: 'start', 438 | align: 'right' 439 | }, { 440 | start: 'start', 441 | align: 'bottom' 442 | }] 443 | }); 444 | 445 | this.player.trigger('start'); 446 | 447 | assert.ok( 448 | this.player.$('.vjs-overlay.vjs-overlay-top'), 449 | 'applies top class' 450 | ); 451 | 452 | assert.ok( 453 | this.player.$('.vjs-overlay.vjs-overlay-right'), 454 | 'applies right class' 455 | ); 456 | 457 | assert.ok( 458 | this.player.$('.vjs-overlay.vjs-overlay-bottom'), 459 | 'applies bottom class' 460 | ); 461 | 462 | assert.ok( 463 | this.player.$('.vjs-overlay.vjs-overlay-left'), 464 | 'applies left class' 465 | ); 466 | }); 467 | 468 | QUnit.test('applies compound alignment class names', function(assert) { 469 | assert.expect(4); 470 | 471 | this.player.overlay({ 472 | overlays: [{ 473 | start: 'start', 474 | align: 'top-left' 475 | }, { 476 | start: 'start', 477 | align: 'top-right' 478 | }, { 479 | start: 'start', 480 | align: 'bottom-left' 481 | }, { 482 | start: 'start', 483 | align: 'bottom-right' 484 | }] 485 | }); 486 | 487 | this.player.trigger('start'); 488 | 489 | assert.ok( 490 | this.player.$('.vjs-overlay.vjs-overlay-top-left'), 491 | 'applies top class' 492 | ); 493 | 494 | assert.ok( 495 | this.player.$('.vjs-overlay.vjs-overlay-top-right'), 496 | 'applies right class' 497 | ); 498 | 499 | assert.ok( 500 | this.player.$('.vjs-overlay.vjs-overlay-bottom-left'), 501 | 'applies bottom class' 502 | ); 503 | 504 | assert.ok( 505 | this.player.$('.vjs-overlay.vjs-overlay-bottom-right'), 506 | 'applies left class' 507 | ); 508 | }); 509 | 510 | QUnit.test('removes time based overlays if the user seeks backward', function(assert) { 511 | assert.expect(2); 512 | 513 | this.player.overlay({ 514 | overlays: [{ 515 | start: 5, 516 | end: 10 517 | }] 518 | }); 519 | 520 | this.updateTime(6); 521 | this.assertOverlayCount(assert, 1); 522 | 523 | this.updateTime(4); 524 | this.assertOverlayCount(assert, 0); 525 | }); 526 | 527 | QUnit.test('applies background styling when showBackground is true', function(assert) { 528 | assert.expect(1); 529 | 530 | this.player.overlay({ 531 | overlays: [{ 532 | start: 'start', 533 | showBackground: true 534 | }] 535 | }); 536 | 537 | this.player.trigger('start'); 538 | 539 | assert.ok( 540 | this.player.$('.vjs-overlay.vjs-overlay-background'), 541 | 'applies background styling' 542 | ); 543 | }); 544 | 545 | QUnit.test('doesn\'t apply background when showBackground is false', function(assert) { 546 | assert.expect(1); 547 | 548 | this.player.overlay({ 549 | overlays: [{ 550 | start: 'start', 551 | showBackground: false 552 | }] 553 | }); 554 | 555 | this.player.trigger('start'); 556 | 557 | assert.notOk( 558 | this.player.$('.vjs-overlay.vjs-overlay-background'), 559 | 'does not apply background styling' 560 | ); 561 | }); 562 | 563 | QUnit.test('attaches bottom aligned overlays to the controlBar', function(assert) { 564 | assert.expect(4); 565 | 566 | this.player.overlay({ 567 | attachToControlBar: true, 568 | overlays: [{ 569 | start: 'start', 570 | align: 'bottom-left' 571 | }, { 572 | start: 'start', 573 | align: 'bottom' 574 | }, { 575 | start: 'start', 576 | align: 'bottom-right' 577 | }, { 578 | start: 'start', 579 | align: 'top-right' 580 | }] 581 | }); 582 | 583 | this.player.trigger('start'); 584 | 585 | assert.ok( 586 | this.player.controlBar.$('.vjs-overlay.vjs-overlay-bottom-left'), 587 | 'bottom-left attaches to control bar' 588 | ); 589 | 590 | assert.ok( 591 | this.player.controlBar.$('.vjs-overlay.vjs-overlay-bottom'), 592 | 'bottom attaches to control bar' 593 | ); 594 | 595 | assert.ok( 596 | this.player.controlBar.$('.vjs-overlay.vjs-overlay-bottom-right'), 597 | 'bottom-right attaches to control bar' 598 | ); 599 | 600 | assert.notOk( 601 | this.player.controlBar.$('.vjs-overlay.vjs-overlay-top-right'), 602 | 'top-right is not attached to control bar' 603 | ); 604 | }); 605 | 606 | QUnit.test('attach only to player when attachToControlbar is false', function(assert) { 607 | assert.expect(2); 608 | 609 | this.player.overlay({ 610 | attachToControlBar: false, 611 | overlays: [{ 612 | start: 'start', 613 | align: 'bottom-left' 614 | }, { 615 | start: 'start', 616 | align: 'bottom' 617 | }] 618 | }); 619 | 620 | assert.notOk( 621 | this.player.controlBar.$('.vjs-overlay.vjs-overlay-bottom-left'), 622 | 'bottom-left is not attached to control bar' 623 | ); 624 | 625 | assert.notOk( 626 | this.player.controlBar.$('.vjs-overlay.vjs-overlay-bottom'), 627 | 'bottom is not attached to control bar' 628 | ); 629 | }); 630 | 631 | QUnit.test('can reinitialize the plugin on reset', function(assert) { 632 | assert.expect(3); 633 | 634 | const overlayPlugin = this.player.overlay({ 635 | attachToControlBar: true, 636 | overlays: [{ 637 | start: 'start', 638 | align: 'bottom-left' 639 | }, { 640 | start: 'start', 641 | align: 'top-right' 642 | }] 643 | }); 644 | 645 | overlayPlugin.reset({ 646 | overlays: [{ 647 | start: 'start', 648 | align: 'top-left' 649 | }] 650 | }); 651 | 652 | assert.notOk( 653 | this.player.$('.vjs-overlay.vjs-overlay-bottom-left'), 654 | 'previous bottom-left aligned overlay removed' 655 | ); 656 | 657 | assert.notOk( 658 | this.player.$('.vjs-overlay.vjs-overlay-top-right'), 659 | 'previous top-right aligned overlay removed' 660 | ); 661 | 662 | assert.ok( 663 | this.player.$('.vjs-overlay.vjs-overlay-top-left'), 664 | 'new top-left overlay added' 665 | ); 666 | }); 667 | 668 | QUnit.test('attach bottom overlay as first child when attachToControlBar is invalid component', function(assert) { 669 | assert.expect(1); 670 | 671 | this.player.overlay({ 672 | attachToControlBar: 'InvalidComponent', 673 | overlays: [{ 674 | start: 'start', 675 | align: 'bottom' 676 | }] 677 | }); 678 | 679 | this.player.trigger('start'); 680 | 681 | assert.equal( 682 | this.player.$('.vjs-overlay.vjs-overlay-bottom'), 683 | this.player.controlBar.el().firstChild, 684 | 'bottom attaches as first child of controlBar' 685 | ); 686 | }); 687 | 688 | QUnit.test('attach top overlay as previous sibling when attachToControlBar is invalid component', function(assert) { 689 | assert.expect(1); 690 | 691 | this.player.overlay({ 692 | attachToControlBar: 'InvalidComponent', 693 | overlays: [{ 694 | start: 'start', 695 | align: 'top' 696 | }] 697 | }); 698 | 699 | this.player.trigger('start'); 700 | 701 | assert.equal( 702 | this.player.$('.vjs-overlay.vjs-overlay-top'), 703 | this.player.controlBar.el().previousSibling, 704 | 'top attaches as previous sibiling of controlBar' 705 | ); 706 | }); 707 | 708 | QUnit.test('attach overlays when attachToControlBar is true', function(assert) { 709 | assert.expect(4); 710 | 711 | const overlayPlugin = this.player.overlay({ 712 | attachToControlBar: true, 713 | overlays: [{ 714 | start: 'start', 715 | align: 'bottom' 716 | }] 717 | }); 718 | 719 | this.player.trigger('start'); 720 | 721 | assert.equal( 722 | this.player.controlBar.$('.vjs-overlay.vjs-overlay-bottom'), 723 | this.player.controlBar.el().firstChild, 724 | 'bottom attaches as first child of control bar' 725 | ); 726 | 727 | overlayPlugin.reset({ 728 | attachToControlBar: true, 729 | overlays: [{ 730 | start: 'start', 731 | align: 'top' 732 | }] 733 | }); 734 | 735 | this.player.trigger('start'); 736 | 737 | assert.equal( 738 | this.player.$('.vjs-overlay.vjs-overlay-top'), 739 | this.player.controlBar.el().previousSibling, 740 | 'top attaches as previous sibiling of controlBar' 741 | ); 742 | 743 | overlayPlugin.reset({ 744 | attachToControlBar: 'RemainingTimeDisplay', 745 | overlays: [{ 746 | start: 'start', 747 | align: 'bottom' 748 | }] 749 | }); 750 | 751 | this.player.trigger('start'); 752 | 753 | assert.equal( 754 | this.player.controlBar.$('.vjs-overlay.vjs-overlay-bottom'), 755 | this.player.controlBar.remainingTimeDisplay.el().previousSibling, 756 | 'bottom attaches as previous sibiling of attachToControlBar component' 757 | ); 758 | 759 | overlayPlugin.reset({ 760 | attachToControlBar: 'RemainingTimeDisplay', 761 | overlays: [{ 762 | start: 'start', 763 | align: 'top' 764 | }] 765 | }); 766 | 767 | this.player.trigger('start'); 768 | 769 | assert.equal( 770 | this.player.$('.vjs-overlay.vjs-overlay-top'), 771 | this.player.controlBar.el().previousSibling, 772 | 'top attaches as previous sibiling of controlBar when using attachToControlBar component' 773 | ); 774 | }); 775 | 776 | QUnit.test('attach overlays as last child when no controls are present', function(assert) { 777 | assert.expect(2); 778 | this.player.controls(false); 779 | 780 | const overlayPlugin = this.player.overlay({ 781 | overlays: [{ 782 | start: 'start', 783 | align: 'bottom' 784 | }] 785 | }); 786 | 787 | this.player.trigger('start'); 788 | 789 | assert.equal( 790 | this.player.$('.vjs-overlay.vjs-overlay-bottom'), 791 | this.player.el().lastChild, 792 | 'bottom attaches as last child of player' 793 | ); 794 | 795 | overlayPlugin.reset({ 796 | overlays: [{ 797 | start: 'start', 798 | align: 'top' 799 | }] 800 | }); 801 | 802 | this.player.trigger('start'); 803 | 804 | assert.equal( 805 | this.player.$('.vjs-overlay.vjs-overlay-top'), 806 | this.player.el().lastChild, 807 | 'top attaches as last child of player' 808 | ); 809 | }); 810 | 811 | QUnit.test('can get all existing overlays with the `get` fn', function(assert) { 812 | assert.expect(1); 813 | this.player.controls(false); 814 | 815 | const overlay = this.player.overlay({ 816 | overlays: [{ 817 | content: 'this is the first overlay', 818 | start: 'start', 819 | align: 'bottom' 820 | }] 821 | }); 822 | 823 | this.player.trigger('start'); 824 | 825 | const overlays = overlay.get(); 826 | 827 | assert.equal(overlays[0].options_.content, 'this is the first overlay'); 828 | }); 829 | 830 | QUnit.test('can add an individual overlay using the `add` fn', function(assert) { 831 | assert.expect(3); 832 | this.player.controls(false); 833 | 834 | const overlay = this.player.overlay({ 835 | overlays: [{ 836 | start: 'start', 837 | align: 'bottom' 838 | }] 839 | }); 840 | 841 | this.player.trigger('start'); 842 | 843 | assert.equal( 844 | this.player.$('.vjs-overlay.vjs-overlay-bottom'), 845 | this.player.el().lastChild, 846 | 'initial bottom overlay is attached as last child of player' 847 | ); 848 | 849 | const addedOverlay = overlay.add({content: 'newly added overlay', start: 'start', align: 'top'}); 850 | 851 | assert.equal(addedOverlay[0].options_.content, 'newly added overlay', 'added overlay object is returned by `add` fn'); 852 | 853 | this.player.trigger('start'); 854 | assert.equal( 855 | this.player.$('.vjs-overlay.vjs-overlay-top'), 856 | this.player.el().lastChild, 857 | 'top gets added as last child of player' 858 | ); 859 | }); 860 | 861 | QUnit.test('can add a list of overlays using the `add` fn', function(assert) { 862 | assert.expect(2); 863 | this.player.controls(false); 864 | 865 | const overlay = this.player.overlay(); 866 | 867 | overlay.add([{start: 'start', align: 'top'}, {start: 'start', align: 'bottom'}]); 868 | 869 | this.player.trigger('start'); 870 | 871 | assert.equal( 872 | this.player.$('.vjs-overlay.vjs-overlay-bottom'), 873 | this.player.el().lastChild, 874 | 'bottom gets added as last child of player' 875 | ); 876 | 877 | assert.equal( 878 | this.player.$('.vjs-overlay.vjs-overlay-top'), 879 | this.player.el().lastChild.previousSibling, 880 | 'top gets added as second last child of player' 881 | ); 882 | }); 883 | 884 | QUnit.test('can remove an overlay using the `remove` fn', function(assert) { 885 | assert.expect(2); 886 | this.player.controls(false); 887 | 888 | const overlay = this.player.overlay({ 889 | overlays: [{ 890 | start: 'start', 891 | align: 'bottom' 892 | }, { 893 | start: 'start', 894 | align: 'top' 895 | }] 896 | }); 897 | 898 | assert.equal( 899 | this.player.$('.vjs-overlay.vjs-overlay-bottom'), 900 | this.player.el().lastChild.previousSibling, 901 | 'bottom gets added as second last child of player' 902 | ); 903 | 904 | overlay.remove(overlay.get()[0]); 905 | 906 | assert.notOk( 907 | this.player.$('.vjs-overlay.vjs-overlay-bottom'), 908 | 'bottom overlay has been removed' 909 | ); 910 | }); 911 | 912 | QUnit.test('`remove` fn does not remove anything if an invalid overlay is passed into it', function(assert) { 913 | assert.expect(2); 914 | this.player.controls(false); 915 | 916 | const overlay = this.player.overlay({ 917 | overlays: [{ 918 | start: 'start', 919 | align: 'bottom' 920 | }] 921 | }); 922 | 923 | assert.equal( 924 | this.player.$('.vjs-overlay.vjs-overlay-bottom'), 925 | this.player.el().lastChild, 926 | 'bottom gets added as last child of player' 927 | ); 928 | 929 | overlay.remove(undefined); 930 | 931 | assert.equal( 932 | this.player.$('.vjs-overlay.vjs-overlay-bottom'), 933 | this.player.el().lastChild, 934 | 'bottom is still last child of player' 935 | ); 936 | }); 937 | --------------------------------------------------------------------------------