diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 5cfe8e69f..8e70bc67c 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -3,4 +3,5 @@ ## Checklist (for PR submitter and reviewers) + - [ ] `CHANGELOG` entry diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f09a73c7e..555a74f2a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,6 +20,17 @@ jobs: with: ref: develop + - name: Install dependencies + run: npm ci + + - name: Read package.json version + uses: actions/github-script@v6 + id: extract-version + with: + script: | + const { version } = require('./package.json') + core.setOutput('packageJsonVersion', version) + - uses: actions/download-artifact@v3 with: path: . @@ -35,6 +46,27 @@ jobs: NPM_DRY_RUN: false NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} + - name: Build documentation + run: npx typedoc + shell: bash + + - name: Authenticate + uses: 'google-github-actions/auth@v2' + with: + credentials_json: ${{ secrets.GCS_CREDENTIALS }} + + - name: Upload docs + uses: 'google-github-actions/upload-cloud-storage@v2' + with: + path: './docs/' + destination: "${{ secrets.GCS_BUCKET }}/player/ui/${{ steps.extract-version.outputs.packageJsonVersion }}" + + - name: Upload docs for major version + uses: 'google-github-actions/upload-cloud-storage@v2' + with: + path: './docs/' + destination: "${{ secrets.GCS_BUCKET }}/player/ui/3" + - name: Notify team run: node .github/scripts/notifySlackTeam.js 'success' 'CHANGELOG.md' ${{ secrets.RELEASE_SUCCESS_SLACK_WEBHOOK }} diff --git a/.gitignore b/.gitignore index a928d0879..79ff64b79 100644 --- a/.gitignore +++ b/.gitignore @@ -416,3 +416,4 @@ jspm_packages # Yarn Integrity file .yarn-integrity +docs diff --git a/CHANGELOG.md b/CHANGELOG.md index cba4a0094..a2a0d4245 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,78 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] ### Changed -- Dpad KeyMap for Android devices +- Dpad keymap for Android devices +## [3.73.0] - 2024-09-06 + +### Added +- `Component` now has a `ViewMode` that can either be `Persistent` or `Temporary` + +### Fixed +- `selectbox` dropdown not closing in Safari when the UI is hidden + +### Changed +- `selectbox` now sets its `ViewMode` to `Persistent` whenever and as long as the select-dropdown is shown +- `uicontainer` and `settingspanel` will no longer auto-hide if there are any components that are in the `Persistent` view mode + +## [3.72.0] - 2024-08-30 + +### Added +- Dutch (nl) subtitles + +## [3.71.0] - 2024-08-28 + +### Added +- Link to API docs in README + +## [3.70.0] - 2024-08-21 + +### Added +- Support for a new placeholder `{adBreakRemainingTime}` in [AdMessageLabel](https://cdn.bitmovin.com/player/ui/3/docs/classes/AdMessageLabel.html) that displays the remaining time in an ad break. [Documentation](https://cdn.bitmovin.com/player/ui/3/docs/functions/StringUtils.replaceAdMessagePlaceholders.html) on usage. + +## [3.69.0] - 2024-08-14 + +### Added +- API doc generation and publishing. The API doc from the UI can be found [here](https://cdn.bitmovin.com/player/ui/3/docs/index.html) + +## [3.67.0] - 2024-07-03 + +### Added +- Missing changelog entries of `3.65.0` and `3.66.0` release versions + +## [3.66.0] - 2024-07-01 + +### Changed +- Playground demo page to include checkbox to enable/disbale ads +- Store basic configuration of playground demo page in localStorage + +## [3.65.0] - 2024-06-24 + +### Added +- Eco Mode toggle button + +## [3.64.0] - 2024-05-28 + +### Added +- `Component` instances are now assigned to their `HTMLElements` for easier accessing + +### Fixed +- Two touch interactions needed to skip an ad or open the click through link + +## [3.63.0] - 2024-05-17 + +### Added +- `QuickSeekButton` control bar component for jumping +/- a configurable number of seconds (10 second default) + +## [3.62.0] - 2024-05-06 + +### Fixed +- No subtitle is shown when switching between different tracks + +## [3.61.0] - 2024-04-23 + +### Fixed +- `ControlBar` not auto-hiding when `UIConfig.disableAutoHideWhenHovered` is set to `true` on some touch screen devices ## [3.60.0] - 2024-04-16 @@ -943,6 +1013,19 @@ Version 2.0 of the UI framework is built for player 7.1. If absolutely necessary ## 1.0.0 (2017-02-03) - First release +[3.73.0]: https://github.com/bitmovin/bitmovin-player-ui/compare/v3.72.0...v3.73.0 +[3.72.0]: https://github.com/bitmovin/bitmovin-player-ui/compare/v3.71.0...v3.72.0 +[3.71.0]: https://github.com/bitmovin/bitmovin-player-ui/compare/v3.70.0...v3.71.0 +[3.70.0]: https://github.com/bitmovin/bitmovin-player-ui/compare/v3.69.0...v3.70.0 +[3.69.0]: https://github.com/bitmovin/bitmovin-player-ui/compare/v3.67.0...v3.69.0 +[3.68.0]: https://github.com/bitmovin/bitmovin-player-ui/compare/v3.67.0...v3.68.0 +[3.67.0]: https://github.com/bitmovin/bitmovin-player-ui/compare/v3.66.0...v3.67.0 +[3.66.0]: https://github.com/bitmovin/bitmovin-player-ui/compare/v3.65.0...v3.66.0 +[3.65.0]: https://github.com/bitmovin/bitmovin-player-ui/compare/v3.64.0...v3.65.0 +[3.64.0]: https://github.com/bitmovin/bitmovin-player-ui/compare/v3.63.0...v3.64.0 +[3.63.0]: https://github.com/bitmovin/bitmovin-player-ui/compare/v3.62.0...v3.63.0 +[3.62.0]: https://github.com/bitmovin/bitmovin-player-ui/compare/v3.61.0...v3.62.0 +[3.61.0]: https://github.com/bitmovin/bitmovin-player-ui/compare/v3.60.0...v3.61.0 [3.60.0]: https://github.com/bitmovin/bitmovin-player-ui/compare/v3.59.0...v3.60.0 [3.59.0]: https://github.com/bitmovin/bitmovin-player-ui/compare/v3.58.0...v3.59.0 [3.58.0]: https://github.com/bitmovin/bitmovin-player-ui/compare/v3.57.0...v3.58.0 diff --git a/README.md b/README.md index 98b4c8279..7b8fe009a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ # Bitmovin Player UI [![npm version](https://badge.fury.io/js/bitmovin-player-ui.svg)](https://badge.fury.io/js/bitmovin-player-ui) [![Build Status](https://app.travis-ci.com/bitmovin/bitmovin-player-ui.svg?branch=develop)](https://app.travis-ci.com/bitmovin/bitmovin-player-ui) -The Bitmovin Adaptive Streaming Player UI -Read more about the usage, along with other important information about the Bitmovin Player at https://bitmovin.com/ and https://bitmovin.com/docs/player. +This repository contains the Bitmovin Player UI framework. +It is designed as a flexible and modularized layer on the player API, enabling customers and users of the player to easily customize the UI to their needs in design, structure, and functionality. It makes it extremely easy and straightforward to add additional control components and we encourage our users to proactively contribute to our codebase. + +Read more about the Framework, its usage and customization possibilities in our [developer documentation](https://developer.bitmovin.com/playback/docs/bitmovin-player-ui) and the [API documentation](https://cdn.bitmovin.com/player/ui/3/docs/index.html). ## Installation @@ -36,270 +38,12 @@ The UI framework is also available in the NPM repository and comes with all sour To take a look at the project, run `gulp serve`. For changes, check our [CHANGELOG](CHANGELOG.md). This UI framework version is for player v8. The UI framework for player v7 can be found in the `support/v2.x` branch. -## Contributing - -Pull requests are welcome! Please check the [contribution guidelines](CONTRIBUTING.md). - -## Introduction - -This repository contains the Bitmovin Player UI framework introduced with the 7.0 release. -It is designed as a flexible and modularized layer on the player API that replaces the old integrated monolithic UI, enabling customers and users of the player to easily customize the UI to their needs in design, structure, and functionality. It makes it extremely easy and straightforward to add additional control components and we encourage our users to proactively contribute to our codebase. - -The framework basically consists of a `UIManager` that handles initialization and destruction of the UI, and components extending the `Component` base class. Components provide specific functionality (e.g. `PlaybackToggleButton`, `ControlBar`, `SeekBar`, `SubtitleOverlay`) and usually consist of two files, a TypeScript `.ts` file containing control code and API interaction with the player, and a SASS `.scss` file containing the visual style. - -A UI is defined by a tree of components, making up the UI *structure*, and their visuals styles, making up the UI *skin*. The root of the structure always starts with a `UIContainer` (or a subclass, e.g. `CastUIContainer`), which is a subclass of `Container` and can contain other components, like any other components extending this class (usually layout components, e.g. `ControlBar`). Components that do not extend the `Container` cannot contain other components and therefore make up the leaves of the UI tree. - -## Customizing the UI - -There are three approaches to customize the UI: - -1. Go with the built-in UI of the player and adjust the styling to your liking with CSS -2. Keep the player managing the UI internally but tell it to load alternative UI CSS/JS files, e.g. your own build from this repository -3. Deactivate the built-in UI and manage your own UI instance externally, e.g. your own build from this repository - -### Styling the built-in UI - -When using the built-in UI, you can style it to your linking with CSS by overwriting our default styles, as documented in our [CSS Class Reference](https://bitmovin.com/docs/player/articles/player-ui-css-class-reference/). - -### Replacing the built-in UI - -#### Internally managed by the player - -It is possible to override which `js` and `css` files the player loads for its internal UI with the `ui` and `ui_css` properties in the `location` section of the player configuration. This is a simple way to supply a customized UI without the overhead of managing an external UI instance, and especially helpful for supplying a custom script which otherwise cannot be overridden like the CSS styles can. The paths to the `ui` (`js`) and `ui_css` (obviously `css`) files can be absolute or relative. Both are optional and do not need to be specified together. - -The player constructs its internal UI instance from the `UIFactory.buildDefaultUI(player)` factory method, so this entry point must exist for this approach to work. - -```js -import { Player } from 'bitmovin-player'; - -const config = { - ..., - location: { - ui: '//domain.tld/path/to/bitmovinplayer-ui.js', - ui_css: 'styles/bitmovinplayer-ui.css', - }, -}; - -const player = new Player(document.getElementById('container-id'), config); -``` - -#### Externally managed - -To use the player with an external custom UI instance, you need to deactivate the built-in UI (set `ui: false`), include the necessary `js` and `css` files into your HTML and create and attach your own UI instance with the `UIManager`. - - * Deactivate the built-in UI by setting `ui: false` in the config of the player ([Player Configuration Guide](https://bitmovin.com/player-documentation/player-configuration/)) - * Build the UI framework (e.g. `gulp build-prod`) and include `bitmovinplayer-ui.min.js` and `bitmovinplayer-ui.min.css` (or their non-minified counterparts) from the `dist` directory - * Create your own UI instance with the `UIFactory` once the player is loaded (or [load a custom UI structure](#building-a-custom-ui-structure)) - -```js -import { Player } from 'bitmovin-player'; -import { UIFactory } from 'bitmovin-player-ui'; - -const config = { - ..., - ui: false, // disable the built-in UI -}; - -const player = new Player(document.getElementById('container-id'), config); -const myUiManager = UIFactory.buildDefaultUI(player); -``` - -### Building a custom UI structure - -Instead of using predefined UI structures from the `UIFactory`, you can easily create a custom structure. For examples on how to create such UI structures, take a look at the `UIFactory` or `DemoFactory`. - -A simple example on how to create a custom UI with our default skin that only contains a playback toggle overlay (an overlay with a large playback toggle button) looks as follows: - -```js -import { Player } from 'bitmovin-player'; -import { PlaybackToggleOverlay, UIContainer, UIManager } from 'bitmovin-player-ui'; - -// Definition of the UI structure -const mySimpleUi = new UIContainer({ - components: [ - new PlaybackToggleOverlay(), - ], -}); - -const player = new Player(document.getElementById('container-id'), config); -const myUiManager = new UIManager(player, mySimpleUi); -``` - -### UIManager - -The `UIManager` manages UI instances and is used to add and remove UIs to/from the player. To add a UI to the player, construct a new instance and pass the `player` object, a UI structure (`UIContainer`) or a list of UI structures with conditions (`UIVariant[]`), and an optional configuration object. To remove a UI from the player, just call `release()` on your UIManager instance. - -```js -import { UIManager } from 'bitmovin-player-ui'; - -// Add UI (e.g. at player initialization) -const myUiManager = new UIManager(player, mySimpleUI); - -// Remove UI (e.g. at player destruction) -myUiManager.release(); -``` - -UIs can be added and removed anytime during the player's lifecycle, which means UIs can be dynamically adjusted to the player, e.g. by listening to events. It is also possible to manage multiple UIs in parallel. - -Here is an example on how to display a special UI in fullscreen mode: - -```js -import { Player, PlayerEvent, ViewMode } from 'bitmovin-player'; -import { UIManager } from 'bitmovin-player-ui'; - -const player = new Player(document.getElementById('container-id'), config); -let myUiManager = new UIManager(player, myWindowUi); - -player.on(PlayerEvent.ViewModeChanged, (event) => { - myUiManager.release(); - if (event.from === ViewMode.Fullscreen) { - myUiManager = new UIManager(player, myFullscreenUi); - } else { - myUiManager = new UIManager(player, myWindowUi); - } -}); -``` - -Alternatively, you can let the `UIManager` handle switching between different UIs by passing in multiple `UIVariant`s: - -```js -import { Player } from 'bitmovin-player'; -import { UIManager } from 'bitmovin-player-ui'; - -const player = new Player(document.getElementById('container-id'), config); -const myUiManager = new UIManager(player, [{ - // Display my fullscreen UI under the condition that the player is in fullscreen mode - ui: myFullscreenUi, - condition: (context) => context.isFullscreen, -}, { - // Display my window UI in all other cases - ui: myWindowUi, -}]); -``` - -There are various conditions upon which the `UIManager` can automatically switch between different UIs, e.g. ad playback and player size. - -#### Factory - -`UIFactory` provides a few predefined UI structures and styles, e.g.: - - * `buildDefaultUI`: The default UI as used by the player by default - * `buildDefaultCastReceiverUI`: A light UI specifically for Google Cast receivers - * `buildDefaultSmallScreenUI`: A light UI specifically for small handheld devices - * `buildDefaultTvUI`: A UI specifically for big screens with remote control ans main input option - -You can easily test and switch between these UIs in the UI playground. - -### Components - -For the list of available components check the `src/ts/components` directory. Each component extends the `Component` base class and adds its own configuration interface and functionality. Components that can container other components as child elements extend the `Container` component. Components are associated to their CSS styles by the `cssClass` config property (prefixed by the `cssPrefix` config property and the `$prefix` SCSS variable). - -Custom components can be easily written by extending any existing component, depending on the required functionality. - -#### Component Configuration - -All components can be directly configured with an optional configuration object that is the first and only parameter of the constructor and defined by an interface. Each component is either accompanied by its own configuration interface (defined in the same `.ts` file and named with the suffix `Config`, e.g. `LabelConfig` for a `Label`), or inherits the configuration interface from its superclass. - -There is currently no way to change these configuration values on an existing UI instance, thus they must be passed directly when creating a custom UI structure. - -The following example creates a very basic UI structure with only two text labels: - -```js -import { Label, UIContainer } from 'bitmovin-player-ui'; - -const myUi = new UIContainer({ - components: [ - new Label({ text: "A label" }), - new Label({ text: "A hidden label", hidden: true }) - ], -}); -``` - -The `UIContainer` is configures with two options, the `components`, an array containing child components, and `cssClasses`, an array with CSS classes to be set on the container. The labels are configures with some `text`, and one label is initially hidden by setting the `hidden` option. - -### UI Configuration - -The `UIManager` takes an optional global configuration object that can be used to configure certain content on the UI. - -```js -import { UIManager } from 'bitmovin-player-ui'; - -const myUiConfig = { - metadata: { - title: 'Video title', - description: 'Video description...', - }, - recommendations: [ - {title: 'Recommendation 1: The best video ever', url: 'http://bitmovin.com', thumbnail: 'http://placehold.it/300x300', duration: 10.4}, - {title: 'Recommendation 2: The second best video', url: 'http://bitmovin.com', thumbnail: 'http://placehold.it/300x300', duration: 64}, - {title: 'Recommendation 3: The third best video of all time', url: 'http://bitmovin.com', thumbnail: 'http://placehold.it/300x300', duration: 195}, - ], -}; - -const myUiManager = new UIManager(player, myUi, myUiConfig); -``` - -All the configuration properties are optional. If `metadata` is set, it overwrites the metadata of the player configuration. If `recommendations` is set, a list of recommendations is shown in the `RecommendationOverlay` at the end of playback. For this to work, the UI must contain a `RecommendationOverlay`, like the default player UI does. - -### UI Localization - -The UI can be localized by calling `UIManager.setLocalizationConfig()` function before initializing a `UIManager`. It ships with English and German translations und uses English by default. Additional translations can be added via the `LocalizationConfig`, where the automatic language detection can also be disabled. - -Please note that the `LocalizationConfig` is a singleton for all UI instances, i.e. it is currently not possible to configure the UI language per `UIManager` instance. This is also the reason why `UIManager.setLocalizationConfig()` _must_ be called before creating a `UIManager` instance for the configuration to be applied as expected. - -```js -const myLocalizationConfig = { - // Automatically select a language fitting the browser language - // (falls back to English if no language that matches the browser language is defined) - language: 'auto', - vocabularies: { - de: { - 'settings': 'Einstellungen', - ... - }, - fr: { - ... - }, - }, -}; - -// First we set the localization configuration -UIManager.setLocalizationConfig(myLocalizationConfig); -// Then we create a UI instance -const myUiManager = new UIManager(...); -``` - -The `UIManager` also has a `localize` function which can be used to translate *custom* labels in user-created component instances. The vocabulary can be extended by adding *custom* keys to `LocalizationConfig.vocabularies`. If a *key* is not present in the vocabulary, `localize` will simply fallback to the *key*. - -```js -const myLocalizationConfig = { - ..., - vocabularies: { - en: { - 'my.custom.key': 'my custom key', - 'this will also act as a key': 'my custom key 2', - }, - de: { - 'my.custom.key': 'my german translation', - }, - }, -}; - -const label1 = new Label({ text: UIManager.localize('my.custom.key') }); - -// This key only exists in the English vocabulary, so it will be translated in English and -// fallback to the key string in any other language -const label2 = new Label({ text: UIManager.localize('this will also act as a key') }); - -// This key does not exist in any vocabulary and will never be localized - it is basically the -// same as setting the text directly (e.g. `{ text: 'This is not included in vocabulary' }`) -const label3 = new Label({ text: UIManager.localize('This is not included in vocabulary') }); -``` - -The default vocabularies along with the keys can be found in the [languages folder](./src/ts/localization/languages). - -### UI Playground +## UI Playground The UI playground can be launched with `gulp serve` and opens a page in a local browser window. On this page, you can switch between different sources and UI styles, trigger API actions and observe events. This page uses BrowserSync to sync the state across multiple tabs and browsers and recompiles and reloads automatically files automatically when any `.scss` or `.ts` files are modified. It makes a helpful tool for developing and testing the UI. + +## Contributing + +Pull requests are welcome! Please check the [contribution guidelines](CONTRIBUTING.md). diff --git a/assets/skin-modern/images/leaf.svg b/assets/skin-modern/images/leaf.svg new file mode 100644 index 000000000..f143e91e9 --- /dev/null +++ b/assets/skin-modern/images/leaf.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/skin-modern/images/quickseek-fastforward.svg b/assets/skin-modern/images/quickseek-fastforward.svg new file mode 100644 index 000000000..b7e6e0034 --- /dev/null +++ b/assets/skin-modern/images/quickseek-fastforward.svg @@ -0,0 +1,2 @@ + + diff --git a/assets/skin-modern/images/quickseek-rewind.svg b/assets/skin-modern/images/quickseek-rewind.svg new file mode 100644 index 000000000..523b8a77e --- /dev/null +++ b/assets/skin-modern/images/quickseek-rewind.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/assets/skin-modern/images/toggleOff.svg b/assets/skin-modern/images/toggleOff.svg new file mode 100644 index 000000000..98254901a --- /dev/null +++ b/assets/skin-modern/images/toggleOff.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/assets/skin-modern/images/toggleOn.svg b/assets/skin-modern/images/toggleOn.svg new file mode 100644 index 000000000..99b015ab3 --- /dev/null +++ b/assets/skin-modern/images/toggleOn.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/package-lock.json b/package-lock.json index 2b153e9e1..3448cf6a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,19 @@ { "name": "bitmovin-player-ui", - "version": "3.60.0", + "version": "3.73.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "bitmovin-player-ui", - "version": "3.60.0", + "version": "3.73.0", "license": "MIT", "devDependencies": { "@inrupt/jest-jsdom-polyfills": "^1.6.0", "@types/jest": "^29.5.0", "@types/jsdom": "^21.1.0", "autoprefixer": "^10.4.14", - "bitmovin-player": "^8.109.0", + "bitmovin-player": "^8.129.0", "browser-sync": "^2.29.0", "browserify": "^17.0.0", "cssnano": "^5.1.15", @@ -40,6 +40,7 @@ "ts-jest": "^29.0.5", "tsify": "^5.0.4", "tslint": "^5.20.1", + "typedoc": "^0.26.5", "typescript": "^5.0.2", "vinyl-buffer": "^1.0.1", "vinyl-source-stream": "^2.0.0", @@ -2917,6 +2918,15 @@ "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", "dev": true }, + "node_modules/@shikijs/core": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.12.1.tgz", + "integrity": "sha512-biCz/mnkMktImI6hMfMX3H9kOeqsInxWEyCHbSlL8C/2TR1FqfmGxTLRNwYCKsyCyxWLbB8rEqXRVZuyxuLFmA==", + "dev": true, + "dependencies": { + "@types/hast": "^3.0.4" + } + }, "node_modules/@sinclair/typebox": { "version": "0.25.24", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", @@ -3068,6 +3078,15 @@ "@types/node": "*" } }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", @@ -3149,6 +3168,12 @@ "integrity": "sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A==", "dev": true }, + "node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==", + "dev": true + }, "node_modules/@types/vinyl": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/vinyl/-/vinyl-2.0.7.tgz", @@ -4502,9 +4527,9 @@ } }, "node_modules/bitmovin-player": { - "version": "8.109.0", - "resolved": "https://registry.npmjs.org/bitmovin-player/-/bitmovin-player-8.109.0.tgz", - "integrity": "sha512-GhsY2XAtcsarI4TqR+Lc5qXPgvKTW2yL932sHifI1dnYzczGO0vbbYkdG+zP/IidNiov8zJSFl84g+ozUFEbXA==", + "version": "8.129.0", + "resolved": "https://registry.npmjs.org/bitmovin-player/-/bitmovin-player-8.129.0.tgz", + "integrity": "sha512-4/bOVjPRa8MtJDeur1cWs2IjIOn5vg3NOwQMcEms6OwurjcPTPP7AWGahlPl6cPf1X7Y83ce5S+V4yfvoAfBQQ==", "dev": true }, "node_modules/bl": { @@ -12899,6 +12924,15 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "dependencies": { + "uc.micro": "^2.0.0" + } + }, "node_modules/load-json-file": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", @@ -13197,6 +13231,12 @@ "es5-ext": "~0.10.2" } }, + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "dev": true + }, "node_modules/make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -13293,6 +13333,29 @@ "node": ">=0.10.0" } }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, "node_modules/matchdep": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz", @@ -13329,6 +13392,12 @@ "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==", "dev": true }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true + }, "node_modules/memoizee": { "version": "0.4.14", "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.14.tgz", @@ -15377,6 +15446,15 @@ "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", "dev": true }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/pure-rand": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.1.tgz", @@ -16675,6 +16753,16 @@ "node": ">=0.10.0" } }, + "node_modules/shiki": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.12.1.tgz", + "integrity": "sha512-nwmjbHKnOYYAe1aaQyEBHvQymJgfm86ZSS7fT8OaPRr4sbAcBNz7PbfAikMEFSDQ6se2j2zobkXvVKcBOm0ysg==", + "dev": true, + "dependencies": { + "@shikijs/core": "1.12.1", + "@types/hast": "^3.0.4" + } + }, "node_modules/side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -18433,6 +18521,64 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", "dev": true }, + "node_modules/typedoc": { + "version": "0.26.5", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.26.5.tgz", + "integrity": "sha512-Vn9YKdjKtDZqSk+by7beZ+xzkkr8T8CYoiasqyt4TTRFy5+UHzL/mF/o4wGBjRF+rlWQHDb0t6xCpA3JNL5phg==", + "dev": true, + "dependencies": { + "lunr": "^2.3.9", + "markdown-it": "^14.1.0", + "minimatch": "^9.0.5", + "shiki": "^1.9.1", + "yaml": "^2.4.5" + }, + "bin": { + "typedoc": "bin/typedoc" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x" + } + }, + "node_modules/typedoc/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/typedoc/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typedoc/node_modules/yaml": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz", + "integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/typescript": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.2.tgz", @@ -18465,6 +18611,12 @@ "node": "*" } }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true + }, "node_modules/uglify-js": { "version": "3.17.4", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", @@ -21961,6 +22113,15 @@ } } }, + "@shikijs/core": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.12.1.tgz", + "integrity": "sha512-biCz/mnkMktImI6hMfMX3H9kOeqsInxWEyCHbSlL8C/2TR1FqfmGxTLRNwYCKsyCyxWLbB8rEqXRVZuyxuLFmA==", + "dev": true, + "requires": { + "@types/hast": "^3.0.4" + } + }, "@sinclair/typebox": { "version": "0.25.24", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", @@ -22106,6 +22267,15 @@ "@types/node": "*" } }, + "@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "requires": { + "@types/unist": "*" + } + }, "@types/istanbul-lib-coverage": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", @@ -22187,6 +22357,12 @@ "integrity": "sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A==", "dev": true }, + "@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==", + "dev": true + }, "@types/vinyl": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/vinyl/-/vinyl-2.0.7.tgz", @@ -23267,9 +23443,9 @@ "dev": true }, "bitmovin-player": { - "version": "8.109.0", - "resolved": "https://registry.npmjs.org/bitmovin-player/-/bitmovin-player-8.109.0.tgz", - "integrity": "sha512-GhsY2XAtcsarI4TqR+Lc5qXPgvKTW2yL932sHifI1dnYzczGO0vbbYkdG+zP/IidNiov8zJSFl84g+ozUFEbXA==", + "version": "8.129.0", + "resolved": "https://registry.npmjs.org/bitmovin-player/-/bitmovin-player-8.129.0.tgz", + "integrity": "sha512-4/bOVjPRa8MtJDeur1cWs2IjIOn5vg3NOwQMcEms6OwurjcPTPP7AWGahlPl6cPf1X7Y83ce5S+V4yfvoAfBQQ==", "dev": true }, "bl": { @@ -29829,6 +30005,15 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, + "linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "requires": { + "uc.micro": "^2.0.0" + } + }, "load-json-file": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", @@ -30064,6 +30249,12 @@ "es5-ext": "~0.10.2" } }, + "lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "dev": true + }, "make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -30143,6 +30334,28 @@ "object-visit": "^1.0.0" } }, + "markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dev": true, + "requires": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + } + } + }, "matchdep": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz", @@ -30175,6 +30388,12 @@ "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==", "dev": true }, + "mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true + }, "memoizee": { "version": "0.4.14", "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.14.tgz", @@ -31707,6 +31926,12 @@ "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", "dev": true }, + "punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true + }, "pure-rand": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.1.tgz", @@ -32740,6 +32965,16 @@ "integrity": "sha512-B1vvzXQlJ77SURr3SIUQ/afh+LwecDKAVKE1wqkBlr2PCHoZDaF6MFD+YX1u9ddQjR4z2CKx1tdqvS2Xfs5h1A==", "dev": true }, + "shiki": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.12.1.tgz", + "integrity": "sha512-nwmjbHKnOYYAe1aaQyEBHvQymJgfm86ZSS7fT8OaPRr4sbAcBNz7PbfAikMEFSDQ6se2j2zobkXvVKcBOm0ysg==", + "dev": true, + "requires": { + "@shikijs/core": "1.12.1", + "@types/hast": "^3.0.4" + } + }, "side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -34098,6 +34333,45 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", "dev": true }, + "typedoc": { + "version": "0.26.5", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.26.5.tgz", + "integrity": "sha512-Vn9YKdjKtDZqSk+by7beZ+xzkkr8T8CYoiasqyt4TTRFy5+UHzL/mF/o4wGBjRF+rlWQHDb0t6xCpA3JNL5phg==", + "dev": true, + "requires": { + "lunr": "^2.3.9", + "markdown-it": "^14.1.0", + "minimatch": "^9.0.5", + "shiki": "^1.9.1", + "yaml": "^2.4.5" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "yaml": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz", + "integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==", + "dev": true + } + } + }, "typescript": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.2.tgz", @@ -34110,6 +34384,12 @@ "integrity": "sha512-K9mwJm/DaB6mRLZfw6q8IMXipcrmuT6yfhYmwhAkuh+81sChuYstYA+znlgaflUPaYUa3odxKPKGw6Vw/lANew==", "dev": true }, + "uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true + }, "uglify-js": { "version": "3.17.4", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", diff --git a/package.json b/package.json index 0b0ee5788..2e4e4996a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bitmovin-player-ui", - "version": "3.60.0", + "version": "3.73.0", "description": "Bitmovin Player UI Framework", "main": "./dist/js/framework/main.js", "types": "./dist/js/framework/main.d.ts", @@ -27,7 +27,7 @@ "@types/jest": "^29.5.0", "@types/jsdom": "^21.1.0", "autoprefixer": "^10.4.14", - "bitmovin-player": "^8.109.0", + "bitmovin-player": "^8.129.0", "browser-sync": "^2.29.0", "browserify": "^17.0.0", "cssnano": "^5.1.15", @@ -54,6 +54,7 @@ "ts-jest": "^29.0.5", "tsify": "^5.0.4", "tslint": "^5.20.1", + "typedoc": "^0.26.5", "typescript": "^5.0.2", "vinyl-buffer": "^1.0.1", "vinyl-source-stream": "^2.0.0", diff --git a/spec/components/selectbox.spec.ts b/spec/components/selectbox.spec.ts new file mode 100644 index 000000000..cbc43941f --- /dev/null +++ b/spec/components/selectbox.spec.ts @@ -0,0 +1,325 @@ +import type { PlayerAPI } from 'bitmovin-player'; + +import type { Component, ViewModeChangedEventArgs } from '../../src/ts/components/component'; +import { ViewMode } from '../../src/ts/components/component'; +import type { ListSelectorConfig } from '../../src/ts/components/listselector'; +import { SelectBox } from '../../src/ts/components/selectbox'; +import type { Event } from '../../src/ts/eventdispatcher'; +import { PlayerUtils } from '../../src/ts/playerutils'; +import type { UIInstanceManager } from '../../src/ts/uimanager'; +import { MockHelper } from '../helper/MockHelper'; +import getUiInstanceManagerMock = MockHelper.getUiInstanceManagerMock; +import getPlayerMock = MockHelper.getPlayerMock; +import generateDOMMock = MockHelper.generateDOMMock; +import PlayerState = PlayerUtils.PlayerState; +import type { DOM } from '../../src/ts/dom'; + +jest.mock('../../src/ts/dom', generateDOMMock); + +describe('SelectBox', () => { + let selectBox: SelectBox; + let playerMock: PlayerAPI; + let uiManagerMock: UIInstanceManager; + + beforeEach(() => { + selectBox = new SelectBox(); + playerMock = getPlayerMock(); + uiManagerMock = getUiInstanceManagerMock(); + }); + + describe('viewMode', () => { + it('should initialize the `ViewMode` to `Temporary`', () => { + expect(selectBox['viewMode']).toEqual(ViewMode.Temporary); + }); + }); + + describe('configure', () => { + test.each` + event + ${'onShow'} + ${'onHide'} + ${'onViewModeChanged'} + `('should subscribe to "$event"', ({ event }) => { + const subscribeSpy = jest.spyOn(selectBox[event as keyof SelectBox] as Event, 'subscribe'); + + selectBox.configure(playerMock, uiManagerMock); + + expect(subscribeSpy).toHaveBeenCalled(); + }); + + test.each` + event + ${'mouseenter'} + ${'mouseleave'} + `('should add a "$event" listener to the DOM element', ({ event }) => { + const onSpy = jest.spyOn(selectBox.getDomElement(), 'on'); + + selectBox.configure(playerMock, uiManagerMock); + + expect(onSpy).toHaveBeenCalledWith(event, expect.any(Function)); + }); + }); + + describe('onViewModeChangedEvent', () => { + let viewModeChangedSpy: jest.Mock, ViewModeChangedEventArgs]>; + + beforeEach(() => { + viewModeChangedSpy = jest.fn(); + selectBox.onViewModeChanged.subscribe(viewModeChangedSpy); + }); + + it('should dispatch the onViewModeChanged event', () => { + selectBox['onViewModeChangedEvent'](ViewMode.Persistent); + + expect(viewModeChangedSpy).toHaveBeenCalledWith(expect.any(SelectBox), { mode: ViewMode.Persistent }); + }); + + it('should not dispatch the onViewModeChanged event if the view mode did not change', () => { + selectBox['onViewModeChangedEvent'](ViewMode.Temporary); + + expect(viewModeChangedSpy).not.toHaveBeenCalled(); + }); + }); + + describe('toDomElement', () => { + test.each` + event + ${'onDisabled'} + ${'onHide'} + `('should subscribe to "$event" to close the dropdown', ({ event }) => { + const subscribeSpy = jest.spyOn(selectBox[event as keyof SelectBox] as Event, 'subscribe'); + + selectBox.getDomElement(); + + expect(subscribeSpy).toHaveBeenCalledWith(selectBox.closeDropdown); + }); + + it('should add event dropdown open event listeners', () => { + const domElement = selectBox.getDomElement(); + + expect(domElement.on).toHaveBeenCalled(); + }); + }); + + describe('configure', () => { + it('should subscribe to player state changes', () => { + const uiContainer = uiManagerMock.getUI(); + + selectBox.configure(playerMock, uiManagerMock); + + expect(uiContainer.onPlayerStateChange().subscribe).toHaveBeenCalledWith(selectBox['onPlayerStateChange']); + }); + }); + + describe('closeDropdown', () => { + it('should call blur on the DOM select element', () => { + const element = document.createElement('select'); + const blurSpy = jest.spyOn(element, 'blur'); + + jest.spyOn(selectBox as any, 'getSelectElement').mockReturnValue(element); + selectBox.closeDropdown(); + + expect(blurSpy).toHaveBeenCalled(); + }); + }); + + describe('onPlayerStateChange', () => { + test.each` + playerState | shouldClose + ${PlayerState.Idle} | ${true} + ${PlayerState.Finished} | ${true} + ${PlayerState.Paused} | ${false} + ${PlayerState.Playing} | ${false} + ${PlayerState.Prepared} | ${false} + `( + `should close the dropdown=$shouldClose when the player state changes to $playerState`, + ({ playerState, shouldClose }) => { + const closeDropdownSpy = jest.spyOn(selectBox, 'closeDropdown'); + + selectBox['onPlayerStateChange'](uiManagerMock.getUI(), playerState); + + if (shouldClose) { + expect(closeDropdownSpy).toHaveBeenCalled(); + } else { + expect(closeDropdownSpy).not.toHaveBeenCalled(); + } + }, + ); + }); + + describe('onDropdownOpened', () => { + it('should clear the existing closed event listener addition timeout', () => { + const clearTimeoutSpy = jest.spyOn(window, 'clearTimeout'); + + selectBox['onDropdownOpened'](); + + expect(clearTimeoutSpy).toHaveBeenCalled(); + }); + + it('should start a timeout to add closed event listeners', () => { + const setTimeoutSpy = jest.spyOn(window, 'setTimeout'); + + selectBox['onDropdownOpened'](); + + expect(setTimeoutSpy).toHaveBeenCalled(); + }); + + it('should change the view mode to `Persistent`', () => { + const onViewModeChangedSpy = jest.fn(); + + selectBox.onViewModeChanged.subscribe(onViewModeChangedSpy); + selectBox['onDropdownOpened'](); + + expect(onViewModeChangedSpy).toHaveBeenCalledWith(expect.any(SelectBox), { mode: ViewMode.Persistent }); + }); + }); + + describe('onDropdownClosed', () => { + it('should clear the closed event listener addition timeout', () => { + const clearTimeoutSpy = jest.spyOn(window, 'clearTimeout'); + + selectBox['onDropdownClosed'](); + + expect(clearTimeoutSpy).toHaveBeenCalled(); + }); + + it('should remove the close event listeners', () => { + const removeDropdownCloseListenersSpy = jest.fn(); + + selectBox['removeDropdownCloseListeners'] = removeDropdownCloseListenersSpy; + selectBox['onDropdownClosed'](); + + expect(removeDropdownCloseListenersSpy).toHaveBeenCalled(); + }); + + it('should change the view mode to `Temporary`', () => { + const onViewModeChangedSpy = jest.fn(); + + selectBox['viewMode'] = ViewMode.Persistent; + selectBox.onViewModeChanged.subscribe(onViewModeChangedSpy); + selectBox['onDropdownClosed'](); + + expect(onViewModeChangedSpy).toHaveBeenCalledWith(expect.any(SelectBox), { mode: ViewMode.Temporary }); + }); + }); + + describe('addDropdownCloseListeners', () => { + let selectElement: DOM; + + beforeEach(() => { + selectElement = { on: jest.fn(), off: jest.fn() } as unknown as DOM; + selectBox['selectElement'] = selectElement; + }); + + it('should remove existing close listeners', () => { + const removeDropdownCloseListenersSpy = jest.fn(); + + selectBox['removeDropdownCloseListeners'] = removeDropdownCloseListenersSpy; + selectBox['addDropdownCloseListeners'](); + + expect(removeDropdownCloseListenersSpy).toHaveBeenCalled(); + }); + + it('should clear the closed event listener addition timeout', () => { + const clearTimeoutSpy = jest.spyOn(window, 'clearTimeout'); + + selectBox['addDropdownCloseListeners'](); + + expect(clearTimeoutSpy).toHaveBeenCalled(); + }); + + it('should add close event listeners to the document', () => { + const addEventListenerSpy = jest.spyOn(document, 'addEventListener'); + + selectBox['addDropdownCloseListeners'](); + + expect(addEventListenerSpy).toHaveBeenCalled(); + }); + + it('should add close event listeners to the select element', () => { + selectBox['addDropdownCloseListeners'](); + + expect(selectElement.on).toHaveBeenCalled(); + }); + + it('should set the removeDropdownCloseListeners', () => { + const initialRemoveDropdownCloseListenersFunction = selectBox['removeDropdownCloseListeners']; + + selectBox['addDropdownCloseListeners'](); + + const newRemoveDropdownCloseListenersFunction = selectBox['removeDropdownCloseListeners']; + + expect(initialRemoveDropdownCloseListenersFunction).not.toEqual(newRemoveDropdownCloseListenersFunction); + }); + }); + + describe('removeDropdownCloseListeners', () => { + let selectElement: DOM; + + beforeEach(() => { + selectElement = { on: jest.fn(), off: jest.fn() } as unknown as DOM; + selectBox['selectElement'] = selectElement; + selectBox['addDropdownCloseListeners'](); + }); + + it('should remove close event listeners to the document', () => { + const removeEventListenerSpy = jest.spyOn(document, 'removeEventListener'); + + selectBox['removeDropdownCloseListeners'](); + + expect(removeEventListenerSpy).toHaveBeenCalled(); + }); + + it('should remove close event listeners from the select element', () => { + selectBox['removeDropdownCloseListeners'](); + + expect(selectElement.off).toHaveBeenCalled(); + }); + }); + + describe('addDropdownOpenedListeners', () => { + let selectElement: DOM; + + beforeEach(() => { + selectElement = { on: jest.fn(), off: jest.fn() } as unknown as DOM; + selectBox['selectElement'] = selectElement; + }); + + it('should remove existing open listeners', () => { + const removeDropdownOpenedListenersSpy = jest.fn(); + + selectBox['removeDropdownOpenedListeners'] = removeDropdownOpenedListenersSpy; + selectBox['addDropdownOpenedListeners'](); + + expect(removeDropdownOpenedListenersSpy).toHaveBeenCalled(); + }); + + it('should add event listener on the select element', () => { + selectBox['addDropdownOpenedListeners'](); + + expect(selectElement.on).toHaveBeenCalled(); + }); + + it('should set the removeDropdownOpenedListeners', () => { + const initialRemoveDropdownOpenedListenersFunction = selectBox['removeDropdownOpenedListeners']; + + selectBox['addDropdownOpenedListeners'](); + + const newRemoveDropdownOpenedListenersFunction = selectBox['removeDropdownOpenedListeners']; + + expect(initialRemoveDropdownOpenedListenersFunction).not.toEqual(newRemoveDropdownOpenedListenersFunction); + }); + }); + + describe('removeDropdownOpenedListeners', () => { + it('should remove opened event listeners from the select element', () => { + const selectElement = { on: jest.fn(), off: jest.fn() } as unknown as DOM; + selectBox['selectElement'] = selectElement; + selectBox['addDropdownOpenedListeners'](); + + selectBox['removeDropdownOpenedListeners'](); + + expect(selectElement.off).toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/components/settingspanel.spec.ts b/spec/components/settingspanel.spec.ts index 81929074c..99cb17ab6 100644 --- a/spec/components/settingspanel.spec.ts +++ b/spec/components/settingspanel.spec.ts @@ -1,25 +1,40 @@ +import type { PlayerAPI } from 'bitmovin-player'; + +import type { Component, ComponentConfig, ViewModeChangedEventArgs } from '../../src/ts/components/component'; +import { ViewMode } from '../../src/ts/components/component'; import { SettingsPanel } from '../../src/ts/components/settingspanel'; -import { MockHelper } from '../helper/MockHelper'; import { SettingsPanelPage } from '../../src/ts/components/settingspanelpage'; +import { EventDispatcher } from '../../src/ts/eventdispatcher'; +import type { UIInstanceManager } from '../../src/ts/uimanager'; +import { MockHelper } from '../helper/MockHelper'; +import getPlayerMock = MockHelper.getPlayerMock; +import getUiInstanceManagerMock = MockHelper.getUiInstanceManagerMock; +import { Label } from '../../src/ts/components/label'; +import { SelectBox } from '../../src/ts/components/selectbox'; +import { SettingsPanelItem } from '../../src/ts/components/settingspanelitem'; +import { VolumeSlider } from '../../src/ts/components/volumeslider'; let settingsPanel: SettingsPanel; describe('SettingsPanel', () => { describe('page navigation', () => { + let playerMock: PlayerAPI; let rootPage: SettingsPanelPage; let firstPage: SettingsPanelPage; let secondPage: SettingsPanelPage; + let uiInstanceManagerMock: UIInstanceManager; beforeEach(() => { + playerMock = getPlayerMock(); rootPage = new SettingsPanelPage({}); firstPage = new SettingsPanelPage({}); secondPage = new SettingsPanelPage({}); - - settingsPanel = new SettingsPanel({ - components: [rootPage, firstPage, secondPage], + settingsPanel = new SettingsPanel({ components: [rootPage, firstPage, secondPage] }); + uiInstanceManagerMock = getUiInstanceManagerMock(); + Object.defineProperty(uiInstanceManagerMock, 'onComponentViewModeChanged', { + value: new EventDispatcher, ViewModeChangedEventArgs>(), }); - const uiInstanceManagerMock = MockHelper.getUiInstanceManagerMock(); - settingsPanel.configure(MockHelper.getPlayerMock(), uiInstanceManagerMock); + settingsPanel.configure(playerMock, uiInstanceManagerMock); }); describe('popSettingsPanelPage', () => { @@ -164,5 +179,58 @@ describe('SettingsPanel', () => { expect(spy).toHaveBeenCalled(); }); }); + + describe('configure', () => { + it('should subscribe to the onComponentViewModeChanged event', () => { + const subscribeSpy = jest.spyOn(uiInstanceManagerMock.onComponentViewModeChanged, 'subscribe'); + + settingsPanel.configure(playerMock, uiInstanceManagerMock); + + expect(subscribeSpy).toHaveBeenCalled(); + }); + }); + + describe('onComponentViewModeChanged', () => { + it('should suspend the hide timeout when a component enters the persistent view mode', () => { + const suspendTimeoutSpy = jest.spyOn(settingsPanel['hideTimeout'], 'suspend'); + + uiInstanceManagerMock.onComponentViewModeChanged.dispatch(undefined, { mode: ViewMode.Persistent }); + + expect(suspendTimeoutSpy).toHaveBeenCalled(); + }); + + it('should resume the hide timeout when the last component left the persistent view mode', () => { + const resumeTimeoutSpy = jest.spyOn(settingsPanel['hideTimeout'], 'resume'); + + uiInstanceManagerMock.onComponentViewModeChanged.dispatch(undefined, { mode: ViewMode.Persistent }); + uiInstanceManagerMock.onComponentViewModeChanged.dispatch(undefined, { mode: ViewMode.Persistent }); + uiInstanceManagerMock.onComponentViewModeChanged.dispatch(undefined, { mode: ViewMode.Persistent }); + + expect(resumeTimeoutSpy).not.toHaveBeenCalled(); + + uiInstanceManagerMock.onComponentViewModeChanged.dispatch(undefined, { mode: ViewMode.Temporary }); + uiInstanceManagerMock.onComponentViewModeChanged.dispatch(undefined, { mode: ViewMode.Temporary }); + + expect(resumeTimeoutSpy).not.toHaveBeenCalled(); + + uiInstanceManagerMock.onComponentViewModeChanged.dispatch(undefined, { mode: ViewMode.Temporary }); + + expect(resumeTimeoutSpy).toHaveBeenCalled(); + }); + }); + + describe('hideHoveredSelectBoxes', () => { + it('should close the dropdown on the select box', () => { + const selectBox = new SelectBox(); + const closeDropdownSpy = jest.spyOn(selectBox, 'closeDropdown'); + + settingsPanel.getActivePage().addComponent(new SettingsPanelItem(new Label(), selectBox)); + settingsPanel.getActivePage().addComponent(new SettingsPanelItem(new Label(), new VolumeSlider())); + + settingsPanel['hideHoveredSelectBoxes'](); + + expect(closeDropdownSpy).toHaveBeenCalled(); + }); + }); }); }); diff --git a/spec/components/uicontainer.spec.ts b/spec/components/uicontainer.spec.ts index 62f60d35b..d188dee10 100644 --- a/spec/components/uicontainer.spec.ts +++ b/spec/components/uicontainer.spec.ts @@ -1,7 +1,13 @@ -import { MockHelper, TestingPlayerAPI } from '../helper/MockHelper'; import { UIContainer } from '../../src/ts/components/uicontainer'; -import { UIInstanceManager } from '../../src/ts/uimanager'; import { PlayerUtils } from '../../src/ts/playerutils'; +import type { UIInstanceManager } from '../../src/ts/uimanager'; +import type { TestingPlayerAPI } from '../helper/MockHelper'; +import { MockHelper } from '../helper/MockHelper'; +import generateDOMMock = MockHelper.generateDOMMock; +import getUiInstanceManagerMock = MockHelper.getUiInstanceManagerMock; +import getPlayerMock = MockHelper.getPlayerMock; + +jest.mock('../../src/ts/dom', generateDOMMock); let playerMock: TestingPlayerAPI; let uiInstanceManagerMock: UIInstanceManager; @@ -9,14 +15,14 @@ let uiInstanceManagerMock: UIInstanceManager; describe('UIContainer', () => { let uiContainer: UIContainer; beforeEach(() => { - playerMock = MockHelper.getPlayerMock(); - uiInstanceManagerMock = MockHelper.getUiInstanceManagerMock(); + playerMock = getPlayerMock(); + uiInstanceManagerMock = getUiInstanceManagerMock(); }); describe('release', () => { beforeEach(() => { uiContainer = new UIContainer({ - hideDelay: -1, // With an hideDelay of -1 the uiHideTimeout and userInteractionEvents never will be initialized + hideDelay: 3000, components: [], }); }); @@ -52,4 +58,44 @@ describe('UIContainer', () => { }); }); }); + + describe('configure', () => { + it('should subscribe to the onComponentViewModeChanged event', () => { + uiContainer = new UIContainer({ hideDelay: 3000, components: [] }); + uiContainer.configure(playerMock, uiInstanceManagerMock); + + expect(uiInstanceManagerMock.onComponentViewModeChanged.subscribe).toHaveBeenCalled(); + }); + }); + + describe('suspendHideTimeout', () => { + it('should suspend the hide timeout', () => { + uiContainer = new UIContainer({ hideDelay: 3000, components: [] }); + uiContainer.configure(playerMock, uiInstanceManagerMock); + + const suspendSpy = jest.spyOn(uiContainer['uiHideTimeout'], 'suspend'); + + uiContainer['suspendHideTimeout'](); + + expect(suspendSpy).toHaveBeenCalled(); + }); + }); + + describe('resumeHideTimeout', () => { + test.each` + hidingPrevented | shouldReset + ${true} | ${false} + ${false} | ${true} + `('should resume and reset=$shouldReset the hide timeout', ({ hidingPrevented, shouldReset }) => { + uiContainer = new UIContainer({ hideDelay: 3000, components: [] }); + uiContainer.configure(playerMock, uiInstanceManagerMock); + + const resume = jest.spyOn(uiContainer['uiHideTimeout'], 'resume'); + + uiContainer['hidingPrevented'] = () => hidingPrevented; + uiContainer['resumeHideTimeout'](); + + expect(resume).toHaveBeenCalledWith(shouldReset); + }); + }); }); diff --git a/spec/helper/MockHelper.ts b/spec/helper/MockHelper.ts index 93a169973..5db46f8a0 100644 --- a/spec/helper/MockHelper.ts +++ b/spec/helper/MockHelper.ts @@ -2,6 +2,7 @@ import { PlayerAPI, PlayerEvent } from 'bitmovin-player'; import { UIInstanceManager } from '../../src/ts/uimanager'; import { DOM } from '../../src/ts/dom'; import { PlayerEventEmitter } from './PlayerEventEmitter'; +import { UIContainer } from '../../src/ts/components/uicontainer'; jest.mock('../../src/ts/dom'); @@ -18,7 +19,14 @@ export namespace MockHelper { }; } + export function getUiMock(): UIContainer { + return { + onPlayerStateChange: jest.fn().mockReturnValue({ subscribe: jest.fn() }), + } as unknown as UIContainer; + } + export function getUiInstanceManagerMock(): UIInstanceManager { + const uiMock = getUiMock(); const UiInstanceManagerMockClass: jest.Mock = jest.fn().mockImplementation(() => ({ onConfigured: getEventDispatcherMock(), getConfig: jest.fn().mockReturnValue({ @@ -29,6 +37,7 @@ export namespace MockHelper { markers: [], }, }), + getUI: () => uiMock, onControlsShow: getEventDispatcherMock(), onControlsHide: getEventDispatcherMock(), onComponentHide: getEventDispatcherMock(), @@ -37,6 +46,7 @@ export namespace MockHelper { onSeek: getEventDispatcherMock(), onSeeked: getEventDispatcherMock(), onRelease: getEventDispatcherMock(), + onComponentViewModeChanged: getEventDispatcherMock(), })); return new UiInstanceManagerMockClass(); diff --git a/src/html/index.html b/src/html/index.html index d200294b3..cf9b18e7c 100644 --- a/src/html/index.html +++ b/src/html/index.html @@ -41,6 +41,14 @@

Player Config

The UI skin or skin type to display. + +
+ +
+ +
+ Schedule client-side ads. +
@@ -97,6 +105,7 @@

+ diff --git a/src/scss/skin-modern/_skin.scss b/src/scss/skin-modern/_skin.scss index 68208b84f..ae1890277 100644 --- a/src/scss/skin-modern/_skin.scss +++ b/src/scss/skin-modern/_skin.scss @@ -6,9 +6,11 @@ @import 'components/controlbar'; @import 'components/button'; @import 'components/playbacktogglebutton'; +@import 'components/quickseekbutton'; @import 'components/fullscreentogglebutton'; @import 'components/vrtogglebutton'; @import 'components/volumetogglebutton'; +@import 'components/ecomodetogglebutton'; @import 'components/seekbar'; @import 'components/watermark'; @import 'components/hugeplaybacktogglebutton'; diff --git a/src/scss/skin-modern/components/_ecomodetogglebutton.scss b/src/scss/skin-modern/components/_ecomodetogglebutton.scss new file mode 100644 index 000000000..bb70a2cf2 --- /dev/null +++ b/src/scss/skin-modern/components/_ecomodetogglebutton.scss @@ -0,0 +1,35 @@ +@import '../variables'; +@import '../mixins'; + +.#{$prefix}-ui-ecomodetogglebutton { + @extend %ui-button; + height: 1em; + min-width: 5em; + + &:hover { + @include svg-icon-shadow; + } + + &.#{$prefix}-on { + background-image: url('../../assets/skin-modern/images/toggleOn.svg'); + background-position: 20px center; + background-size: 45% auto; + margin-left: 2%; + } + + &.#{$prefix}-off { + background-image: url('../../assets/skin-modern/images/toggleOff.svg'); + background-position: 20px center; + background-size: 45% auto; + } +} + +#ecomodelabel::before { + background-image: url('../../assets/skin-modern/images/leaf.svg'); + background-repeat: no-repeat; + background-size: 1.7em auto; + content: ' '; + display: inline-block; + height: 1.5em; + width: 2em; +} diff --git a/src/scss/skin-modern/components/_label.scss b/src/scss/skin-modern/components/_label.scss index 70e26540d..2d1865357 100644 --- a/src/scss/skin-modern/components/_label.scss +++ b/src/scss/skin-modern/components/_label.scss @@ -12,3 +12,10 @@ .#{$prefix}-ui-label { @extend %ui-label; } + +.#{$prefix}-ui-label-savedEnergy { + @extend %ui-label; + font-size: 0.8em; + color: #1fabe2; + margin-left: 2.2em; +} diff --git a/src/scss/skin-modern/components/_quickseekbutton.scss b/src/scss/skin-modern/components/_quickseekbutton.scss new file mode 100644 index 000000000..89d8a091a --- /dev/null +++ b/src/scss/skin-modern/components/_quickseekbutton.scss @@ -0,0 +1,18 @@ +@import '../variables'; +@import '../mixins'; + +.#{$prefix}-ui-quickseekbutton { + @extend %ui-button; + + &:hover { + @include svg-icon-shadow; + } + + &[data-#{$prefix}-seek-direction='forward'] { + background-image: url('../../assets/skin-modern/images/quickseek-fastforward.svg'); + } + + &[data-#{$prefix}-seek-direction='rewind'] { + background-image: url('../../assets/skin-modern/images/quickseek-rewind.svg'); + } +} diff --git a/src/ts/arrayutils.ts b/src/ts/arrayutils.ts index 3390843cf..c2b6b0dfc 100644 --- a/src/ts/arrayutils.ts +++ b/src/ts/arrayutils.ts @@ -1,3 +1,6 @@ +/** + * @category Utils + */ export namespace ArrayUtils { /** * Removes an item from an array. diff --git a/src/ts/audiotrackutils.ts b/src/ts/audiotrackutils.ts index 1676ca859..93ad0a02e 100644 --- a/src/ts/audiotrackutils.ts +++ b/src/ts/audiotrackutils.ts @@ -7,6 +7,8 @@ import { i18n } from './localization/i18n'; * Helper class to handle all audio tracks related events * * This class listens to player events as well as the `ListSelector` event if selection changed + * + * @category Utils */ export class AudioTrackSwitchHandler { diff --git a/src/ts/browserutils.ts b/src/ts/browserutils.ts index a274cbbfb..fd4092992 100644 --- a/src/ts/browserutils.ts +++ b/src/ts/browserutils.ts @@ -4,6 +4,9 @@ declare global { } } +/** + * @category Utils + */ export class BrowserUtils { // isMobile only needs to be evaluated once (it cannot change during a browser session) // Mobile detection according to Mozilla recommendation: "In summary, we recommend looking for the string “Mobi” diff --git a/src/ts/components/adclickoverlay.ts b/src/ts/components/adclickoverlay.ts index 3ad0ebd94..9f9b50ca3 100644 --- a/src/ts/components/adclickoverlay.ts +++ b/src/ts/components/adclickoverlay.ts @@ -1,11 +1,20 @@ -import { ClickOverlay } from './clickoverlay'; +import { ClickOverlay, ClickOverlayConfig } from './clickoverlay'; import { UIInstanceManager } from '../uimanager'; import { Ad, AdEvent, PlayerAPI } from 'bitmovin-player'; /** * A simple click capture overlay for clickThroughUrls of ads. + * + * @category Components */ export class AdClickOverlay extends ClickOverlay { + constructor(config: ClickOverlayConfig = {}) { + super(config); + + this.config = this.mergeConfig(config, { + acceptsTouchWithUiHidden: true, + }, this.config); + } configure(player: PlayerAPI, uimanager: UIInstanceManager): void { super.configure(player, uimanager); diff --git a/src/ts/components/admessagelabel.ts b/src/ts/components/admessagelabel.ts index 476f03111..63c0bd537 100644 --- a/src/ts/components/admessagelabel.ts +++ b/src/ts/components/admessagelabel.ts @@ -6,6 +6,8 @@ import { i18n } from '../localization/i18n'; /** * A label that displays a message about a running ad, optionally with a countdown. + * + * @category Components */ export class AdMessageLabel extends Label { diff --git a/src/ts/components/adskipbutton.ts b/src/ts/components/adskipbutton.ts index b021f6810..e77fd23cd 100644 --- a/src/ts/components/adskipbutton.ts +++ b/src/ts/components/adskipbutton.ts @@ -5,6 +5,8 @@ import { AdEvent, LinearAd, PlayerAPI } from 'bitmovin-player'; /** * Configuration interface for the {@link AdSkipButton}. + * + * @category Configs */ export interface AdSkipButtonConfig extends ButtonConfig { /** @@ -21,6 +23,8 @@ export interface AdSkipButtonConfig extends ButtonConfig { /** * A button that is displayed during ads and can be used to skip the ad. + * + * @category Buttons */ export class AdSkipButton extends Button { @@ -31,6 +35,7 @@ export class AdSkipButton extends Button { cssClass: 'ui-button-ad-skip', untilSkippableMessage: 'Skip ad in {remainingTime}', skippableMessage: 'Skip ad', + acceptsTouchWithUiHidden: true, }, this.config); } diff --git a/src/ts/components/airplaytogglebutton.ts b/src/ts/components/airplaytogglebutton.ts index b2399d133..4bdeb6335 100644 --- a/src/ts/components/airplaytogglebutton.ts +++ b/src/ts/components/airplaytogglebutton.ts @@ -5,6 +5,8 @@ import { i18n } from '../localization/i18n'; /** * A button that toggles Apple AirPlay. + * + * @category Buttons */ export class AirPlayToggleButton extends ToggleButton { diff --git a/src/ts/components/audioqualityselectbox.ts b/src/ts/components/audioqualityselectbox.ts index 05e6e8279..59d6b407c 100644 --- a/src/ts/components/audioqualityselectbox.ts +++ b/src/ts/components/audioqualityselectbox.ts @@ -6,6 +6,8 @@ import { i18n } from '../localization/i18n'; /** * A select box providing a selection between 'auto' and the available audio qualities. + * + * @category Components */ export class AudioQualitySelectBox extends SelectBox { diff --git a/src/ts/components/audiotracklistbox.ts b/src/ts/components/audiotracklistbox.ts index 050893429..5124fe8ac 100644 --- a/src/ts/components/audiotracklistbox.ts +++ b/src/ts/components/audiotracklistbox.ts @@ -5,6 +5,8 @@ import { PlayerAPI } from 'bitmovin-player'; /** * A element that is similar to a select box where the user can select a subtitle + * + * @category Components */ export class AudioTrackListBox extends ListBox { diff --git a/src/ts/components/audiotrackselectbox.ts b/src/ts/components/audiotrackselectbox.ts index 89235835a..687b73319 100644 --- a/src/ts/components/audiotrackselectbox.ts +++ b/src/ts/components/audiotrackselectbox.ts @@ -6,6 +6,8 @@ import { PlayerAPI } from 'bitmovin-player'; /** * A select box providing a selection between available audio tracks (e.g. different languages). + * + * @category Components */ export class AudioTrackSelectBox extends SelectBox { diff --git a/src/ts/components/bufferingoverlay.ts b/src/ts/components/bufferingoverlay.ts index d6d51201a..570141534 100644 --- a/src/ts/components/bufferingoverlay.ts +++ b/src/ts/components/bufferingoverlay.ts @@ -6,6 +6,8 @@ import { PlayerAPI } from 'bitmovin-player'; /** * Configuration interface for the {@link BufferingOverlay} component. + * + * @category Configs */ export interface BufferingOverlayConfig extends ContainerConfig { /** @@ -18,6 +20,8 @@ export interface BufferingOverlayConfig extends ContainerConfig { /** * Overlays the player and displays a buffering indicator. + * + * @category Components */ export class BufferingOverlay extends Container { diff --git a/src/ts/components/button.ts b/src/ts/components/button.ts index b0855802f..d5fe59f16 100644 --- a/src/ts/components/button.ts +++ b/src/ts/components/button.ts @@ -5,16 +5,31 @@ import { LocalizableText , i18n } from '../localization/i18n'; /** * Configuration interface for a {@link Button} component. + * + * @category Configs */ export interface ButtonConfig extends ComponentConfig { /** * The text as string or localize callback on the button. */ text?: LocalizableText; + /** + * WCAG20 standard for defining info about the component (usually the name) + */ + ariaLabel?: LocalizableText; + + /** + * Specifies whether the first touch event received by the {@link UIContainer} should be prevented or not. + * + * Default: false + */ + acceptsTouchWithUiHidden?: boolean; } /** * A simple clickable button. + * + * @category Components */ export class Button extends Component { @@ -29,6 +44,7 @@ export class Button extends Component { cssClass: 'ui-button', role: 'button', tabIndex: 0, + acceptsTouchWithUiHidden: false, } as Config, this.config); } @@ -50,7 +66,7 @@ export class Button extends Component { } // Create the button element with the text label - let buttonElement = new DOM('button', buttonElementAttributes).append(new DOM('span', { + let buttonElement = new DOM('button', buttonElementAttributes, this).append(new DOM('span', { 'class': this.prefixCss('label'), }).html(i18n.performLocalization(this.config.text))); diff --git a/src/ts/components/caststatusoverlay.ts b/src/ts/components/caststatusoverlay.ts index 6b91f74af..a7ad23e57 100644 --- a/src/ts/components/caststatusoverlay.ts +++ b/src/ts/components/caststatusoverlay.ts @@ -6,6 +6,8 @@ import { i18n } from '../localization/i18n'; /** * Overlays the player and displays the status of a Cast session. + * + * @category Components */ export class CastStatusOverlay extends Container { diff --git a/src/ts/components/casttogglebutton.ts b/src/ts/components/casttogglebutton.ts index 0bc1826c3..c7f55b250 100644 --- a/src/ts/components/casttogglebutton.ts +++ b/src/ts/components/casttogglebutton.ts @@ -5,6 +5,8 @@ import { i18n } from '../localization/i18n'; /** * A button that toggles casting to a Cast receiver. + * + * @category Buttons */ export class CastToggleButton extends ToggleButton { diff --git a/src/ts/components/castuicontainer.ts b/src/ts/components/castuicontainer.ts index 7b4faee89..3cf2dec9c 100644 --- a/src/ts/components/castuicontainer.ts +++ b/src/ts/components/castuicontainer.ts @@ -6,6 +6,8 @@ import { PlayerAPI } from 'bitmovin-player'; /** * The base container for Cast receivers that contains all of the UI and takes care that the UI is shown on * certain playback events. + * + * @category Containers */ export class CastUIContainer extends UIContainer { diff --git a/src/ts/components/clickoverlay.ts b/src/ts/components/clickoverlay.ts index c997b69fc..311713bd6 100644 --- a/src/ts/components/clickoverlay.ts +++ b/src/ts/components/clickoverlay.ts @@ -2,6 +2,8 @@ import {Button, ButtonConfig} from './button'; /** * Configuration interface for a {@link ClickOverlay}. + * + * @category Configs */ export interface ClickOverlayConfig extends ButtonConfig { /** @@ -12,6 +14,8 @@ export interface ClickOverlayConfig extends ButtonConfig { /** * A click overlay that opens an url in a new tab if clicked. + * + * @category Components */ export class ClickOverlay extends Button { diff --git a/src/ts/components/closebutton.ts b/src/ts/components/closebutton.ts index 70913db16..0daf3c230 100644 --- a/src/ts/components/closebutton.ts +++ b/src/ts/components/closebutton.ts @@ -6,6 +6,8 @@ import { i18n } from '../localization/i18n'; /** * Configuration interface for the {@link CloseButton}. + * + * @category Configs */ export interface CloseButtonConfig extends ButtonConfig { /** @@ -16,6 +18,8 @@ export interface CloseButtonConfig extends ButtonConfig { /** * A button that closes (hides) a configured component. + * + * @category Buttons */ export class CloseButton extends Button { diff --git a/src/ts/components/component.ts b/src/ts/components/component.ts index 4cedd5954..50cfa79f6 100644 --- a/src/ts/components/component.ts +++ b/src/ts/components/component.ts @@ -8,6 +8,8 @@ import { i18n, LocalizableText } from '../localization/i18n'; /** * Base configuration interface for a component. * Should be extended by components that want to add additional configuration options. + * + * @category Configs */ export interface ComponentConfig { /** @@ -71,9 +73,31 @@ export interface ComponentHoverChangedEventArgs extends NoArgs { hovered: boolean; } +export enum ViewMode { + /** + * Indicates that the component has entered a view mode where it must stay visible. Auto-hiding of this component + * must be disabled as long as it resides in this state. + */ + Persistent = 'persistent', + + /** + * The control can be hidden at any time. + */ + Temporary = 'temporary', +} + +export interface ViewModeChangedEventArgs extends NoArgs { + /** + * The `ViewMode` the control is currently in. + */ + mode: ViewMode; +} + /** * The base class of the UI framework. * Each component must extend this class and optionally the config interface. + * + * @category Components */ export class Component { @@ -114,6 +138,11 @@ export class Component { */ private hovered: boolean; + /** + * The current view mode of the component. + */ + private viewMode: ViewMode; + /** * The list of events that this component offers. These events should always be private and only directly * accessed from within the implementing component. @@ -175,6 +204,7 @@ export class Component { private componentEvents = { onShow: new EventDispatcher, NoArgs>(), onHide: new EventDispatcher, NoArgs>(), + onViewModeChanged: new EventDispatcher, ViewModeChangedEventArgs>(), onHoverChanged: new EventDispatcher, ComponentHoverChangedEventArgs>(), onEnabled: new EventDispatcher, NoArgs>(), onDisabled: new EventDispatcher, NoArgs>(), @@ -196,6 +226,7 @@ export class Component { hidden: false, disabled: false, }, {}); + this.viewMode = ViewMode.Temporary; } /** @@ -235,20 +266,13 @@ export class Component { * @param uimanager the UIInstanceManager that manages this component */ configure(player: PlayerAPI, uimanager: UIInstanceManager): void { - this.onShow.subscribe(() => { - uimanager.onComponentShow.dispatch(this); - }); - this.onHide.subscribe(() => { - uimanager.onComponentHide.dispatch(this); - }); + this.onShow.subscribe(() => uimanager.onComponentShow.dispatch(this)); + this.onHide.subscribe(() => uimanager.onComponentHide.dispatch(this)); + this.onViewModeChanged.subscribe((_, args) => uimanager.onComponentViewModeChanged.dispatch(this, args)); // Track the hovered state of the element - this.getDomElement().on('mouseenter', () => { - this.onHoverChangedEvent(true); - }); - this.getDomElement().on('mouseleave', () => { - this.onHoverChangedEvent(false); - }); + this.getDomElement().on('mouseenter', () => this.onHoverChangedEvent(true)); + this.getDomElement().on('mouseleave', () => this.onHoverChangedEvent(false)); } /** @@ -272,7 +296,7 @@ export class Component { 'id': this.config.id, 'class': this.getCssClasses(), 'role': this.config.role, - }); + }, this); return element; } @@ -485,6 +509,19 @@ export class Component { this.componentEvents.onDisabled.dispatch(this); } + /** + * Fires the onViewModeChanged event. + * See the detailed explanation on event architecture on the {@link #componentEvents events list}. + */ + protected onViewModeChangedEvent(mode: ViewMode): void { + if (this.viewMode === mode) { + return; + } + + this.viewMode = mode; + this.componentEvents.onViewModeChanged.dispatch(this, { mode }); + } + /** * Fires the onHoverChanged event. * See the detailed explanation on event architecture on the {@link #componentEvents events list}. @@ -537,4 +574,12 @@ export class Component { get onHoverChanged(): Event, ComponentHoverChangedEventArgs> { return this.componentEvents.onHoverChanged.getEvent(); } + + /** + * Gets the event that is fired when the `ViewMode` of this component has changed. + * @returns {Event, ViewModeChangedEventArgs>} + */ + get onViewModeChanged(): Event, ViewModeChangedEventArgs> { + return this.componentEvents.onViewModeChanged.getEvent(); + } } \ No newline at end of file diff --git a/src/ts/components/container.ts b/src/ts/components/container.ts index f9337dac2..b14d79df1 100644 --- a/src/ts/components/container.ts +++ b/src/ts/components/container.ts @@ -1,10 +1,12 @@ -import {ComponentConfig, Component} from './component'; +import { ComponentConfig, Component, ViewModeChangedEventArgs, ViewMode } from './component'; import {DOM} from '../dom'; import {ArrayUtils} from '../arrayutils'; import { i18n } from '../localization/i18n'; /** * Configuration interface for a {@link Container}. + * + * @category Configs */ export interface ContainerConfig extends ComponentConfig { /** @@ -31,6 +33,8 @@ export interface ContainerConfig extends ComponentConfig { * * * + * + * @category Components */ export class Container extends Component { @@ -40,6 +44,7 @@ export class Container extends Component private innerContainerElement: DOM; private componentsToAdd: Component[]; private componentsToRemove: Component[]; + private componentsInPersistentViewMode: number; constructor(config: Config) { super(config); @@ -51,6 +56,7 @@ export class Container extends Component this.componentsToAdd = []; this.componentsToRemove = []; + this.componentsInPersistentViewMode = 0; } /** @@ -121,7 +127,7 @@ export class Container extends Component 'class': this.getCssClasses(), 'role': this.config.role, 'aria-label': i18n.performLocalization(this.config.ariaLabel), - }); + }, this); // Create the inner container element (the inner
) that will contain the components let innerContainer = new DOM(this.config.tag, { @@ -138,4 +144,28 @@ export class Container extends Component return containerElement; } + + protected suspendHideTimeout(): void { + // to be implemented in subclass + } + + protected resumeHideTimeout(): void { + // to be implemented in subclass + } + + protected trackComponentViewMode(mode: ViewMode) { + if (mode === ViewMode.Persistent) { + this.componentsInPersistentViewMode++; + } else if (mode === ViewMode.Temporary) { + this.componentsInPersistentViewMode = Math.max(this.componentsInPersistentViewMode - 1, 0); + } + + if (this.componentsInPersistentViewMode > 0) { + // There is at least one component that must not be hidden, + // therefore the hide timeout must be suspended + this.suspendHideTimeout(); + } else { + this.resumeHideTimeout(); + } + } } \ No newline at end of file diff --git a/src/ts/components/controlbar.ts b/src/ts/components/controlbar.ts index 04e1d4045..8de8388e1 100644 --- a/src/ts/components/controlbar.ts +++ b/src/ts/components/controlbar.ts @@ -10,6 +10,8 @@ import {SettingsPanel} from './settingspanel'; /** * Configuration interface for the {@link ControlBar}. + * + * @category Configs */ export interface ControlBarConfig extends ContainerConfig { // nothing yet @@ -18,6 +20,8 @@ export interface ControlBarConfig extends ContainerConfig { /** * A container for main player control components, e.g. play toggle button, seek bar, volume control, fullscreen toggle * button. + * + * @category Components */ export class ControlBar extends Container { @@ -39,10 +43,11 @@ export class ControlBar extends Container { let hoverStackCount = 0; let isSettingsPanelShown = false; - // only enabling this for non-mobile platforms without touch input. enabling this - // for touch devices causes the UI to not disappear after hideDelay seconds. + // Only enabling this for platforms without touch input. + // Enabling this for touch devices causes the UI to not disappear after hideDelay seconds, + // because `mouseleave` event is not emitted. // Instead, it will stay visible until another manual interaction is performed. - if (uimanager.getConfig().disableAutoHideWhenHovered && !BrowserUtils.isMobile) { + if (uimanager.getConfig().disableAutoHideWhenHovered && !BrowserUtils.isTouchSupported) { // Track hover status of child components UIUtils.traverseTree(this, (component) => { // Do not track hover status of child containers or spacers, only of 'real' controls diff --git a/src/ts/components/ecomodecontainer.ts b/src/ts/components/ecomodecontainer.ts new file mode 100644 index 000000000..02ea81ead --- /dev/null +++ b/src/ts/components/ecomodecontainer.ts @@ -0,0 +1,129 @@ +import { PlayerAPI, SegmentPlaybackEvent, VideoQuality } from 'bitmovin-player'; +import { i18n } from '../localization/i18n'; +import { Container, ContainerConfig } from './container'; +import { EcoModeToggleButton } from './ecomodetogglebutton'; +import { Label, LabelConfig } from './label'; +import { SettingsPanelItem } from './settingspanelitem'; + +/** + * @category Containers + */ +export class EcoModeContainer extends Container { + private ecoModeSavedEmissionsItem: SettingsPanelItem; + private ecoModeToggleButtonItem: SettingsPanelItem; + private emissionsSavedLabel: Label; + private savedEmissons = 0; + private currentEnergyEmission: number; + + constructor(config: ContainerConfig = {}) { + super(config); + + const ecoModeToggleButton = new EcoModeToggleButton(); + const labelEcoMode = new Label({ + text: i18n.getLocalizer('ecoMode.title'), + for: ecoModeToggleButton.getConfig().id, + id: 'ecomodelabel', + }); + this.emissionsSavedLabel = new Label({ + text: `${this.savedEmissons.toFixed(4)} gCO2`, + cssClass: 'ui-label-savedEnergy', + }); + + this.ecoModeToggleButtonItem = new SettingsPanelItem(labelEcoMode, ecoModeToggleButton); + this.ecoModeSavedEmissionsItem = new SettingsPanelItem('Saved Emissions', this.emissionsSavedLabel, { + hidden: true, + }); + + this.addComponent(this.ecoModeToggleButtonItem); + this.addComponent(this.ecoModeSavedEmissionsItem); + + ecoModeToggleButton.onToggleOn.subscribe(() => { + this.ecoModeSavedEmissionsItem.show(); + this.onToggleCallback(); + }); + + ecoModeToggleButton.onToggleOff.subscribe(() => { + this.ecoModeSavedEmissionsItem.hide(); + this.onToggleCallback(); + }); + } + + private onToggleCallback: () => void; + + public setOnToggleCallback(callback: () => void) { + this.onToggleCallback = callback; + } + + configure(player: PlayerAPI): void { + player.on(player.exports.PlayerEvent.SegmentPlayback, (segment: SegmentPlaybackEvent) => { + if (!segment.mimeType.includes('video')) { + return; + } + + const { height, width, bitrate, frameRate } = segment.mediaInfo; + const { + height: maxHeight, + bitrate: maxBitrate, + width: maxWidth, + } = this.getMaxQualityAvailable(player.getAvailableVideoQualities()); + + const currentEnergyKwh = this.calculateEnergyConsumption(frameRate, height, width, bitrate, segment.duration); + + const maxEnergyKwh = this.calculateEnergyConsumption( + frameRate, + maxHeight, + maxWidth, + maxBitrate, + segment.duration, + ); + + if (this.ecoModeSavedEmissionsItem.isShown()) { + this.updateSavedEmissions(currentEnergyKwh, maxEnergyKwh, this.emissionsSavedLabel); + } + }); + } + + updateSavedEmissions( + currentEnergyConsuption: number, + maxEnergyConsuption: number, + emissionsSavedLabel: Label, + ) { + // 475 is the average carbon intensity of all countries in gCO2/kWh + const averageCarbonIntensity = 475; + + this.currentEnergyEmission = currentEnergyConsuption * averageCarbonIntensity; + const maxEnergyEmisson = maxEnergyConsuption * averageCarbonIntensity; + this.savedEmissons += maxEnergyEmisson - this.currentEnergyEmission; + emissionsSavedLabel.setText(this.savedEmissons.toFixed(4) + ' gCO2'); + } + + /** + * The calculations are based on the following paper: https://arxiv.org/pdf/2210.05444.pdf + */ + calculateEnergyConsumption(fps: number, height: number, width: number, bitrate: number, duration: number): number { + const fpsWeight = 0.035; + const pixeldWeight = 5.76e-9; + const birateWeight = 6.97e-6; + const constantOffset = 8.52; + const bitrateInternetWeight = 3.24e-5; + const internetConnectionOffset = 1.15; + const videoCodec = 4.16; + + const energyConsumptionW = + fpsWeight * fps + + pixeldWeight * height * width + + (birateWeight + bitrateInternetWeight) * (bitrate / 1000) + + videoCodec + + constantOffset + + internetConnectionOffset; + + // Convert energy consumption from Watts (W) to Kilowatt-hours (kWh) for the given time duration of the segment + const energyConsumptionKwh = (energyConsumptionW * duration) / 3.6e6; + + return energyConsumptionKwh; + } + getMaxQualityAvailable(availableVideoQualities: VideoQuality[]) { + const sortedQualities = availableVideoQualities.sort((a, b) => a.bitrate - b.bitrate); + return sortedQualities[sortedQualities.length - 1]; + } +} diff --git a/src/ts/components/ecomodetogglebutton.ts b/src/ts/components/ecomodetogglebutton.ts new file mode 100644 index 000000000..b5cde476e --- /dev/null +++ b/src/ts/components/ecomodetogglebutton.ts @@ -0,0 +1,89 @@ +import { ToggleButton, ToggleButtonConfig } from './togglebutton'; +import { UIInstanceManager } from '../uimanager'; +import { DynamicAdaptationConfig, PlayerAPI, VideoQualityChangedEvent } from 'bitmovin-player'; +import { i18n } from '../localization/i18n'; + +/** + * @category Buttons + */ +export class EcoModeToggleButton extends ToggleButton { + private adaptationConfig: DynamicAdaptationConfig; + + constructor(config: ToggleButtonConfig = {}) { + super(config); + + const defaultConfig: ToggleButtonConfig = { + text: i18n.getLocalizer('ecoMode'), + cssClass: 'ui-ecomodetogglebutton', + onClass: 'on', + offClass: 'off', + ariaLabel: i18n.getLocalizer('ecoMode'), + }; + + this.config = this.mergeConfig(config, defaultConfig, this.config); + } + + configure(player: PlayerAPI, uimanager: UIInstanceManager): void { + super.configure(player, uimanager); + + if (this.areAdaptationApisAvailable(player)) { + this.onClick.subscribe(() => { + this.toggle(); + }); + + this.onToggleOn.subscribe(() => { + this.enableEcoMode(player); + player.setVideoQuality('auto'); + }); + + this.onToggleOff.subscribe(() => { + this.disableEcoMode(player); + }); + + player.on(player.exports.PlayerEvent.VideoQualityChanged, (quality: VideoQualityChangedEvent) => { + if (quality.targetQuality.id !== 'auto') { + this.off(); + this.disableEcoMode(player); + } + }); + } else { + super.disable(); + } + + } + + private areAdaptationApisAvailable(player: PlayerAPI): boolean { + const isGetConfigAvailable = Boolean(player.adaptation.getConfig && typeof player.adaptation.getConfig === 'function'); + const isSetConfigAvailable = Boolean(player.adaptation.setConfig && typeof player.adaptation.setConfig === 'function'); + + return Boolean(player.adaptation && isGetConfigAvailable && isSetConfigAvailable); + } + + enableEcoMode(player: PlayerAPI): void { + this.adaptationConfig = player.adaptation.getConfig(); + const codec = player.getAvailableVideoQualities()[0].codec; + + if (codec.includes('avc')) { + player.adaptation.setConfig({ + resolution: { maxSelectableVideoHeight: 720 }, + limitToPlayerSize: true, + }); + } + if (codec.includes('hvc') || codec.includes('hev')) { + player.adaptation.setConfig({ + resolution: { maxSelectableVideoHeight: 1080 }, + limitToPlayerSize: true, + }); + } + if (codec.includes('av1') || codec.includes('av01')) { + player.adaptation.setConfig({ + resolution: { maxSelectableVideoHeight: 1440 }, + limitToPlayerSize: true, + }); + } + } + + disableEcoMode(player: PlayerAPI): void { + player.adaptation.setConfig(this.adaptationConfig); + } +} diff --git a/src/ts/components/errormessageoverlay.ts b/src/ts/components/errormessageoverlay.ts index 189cebb2e..b09de8ecf 100644 --- a/src/ts/components/errormessageoverlay.ts +++ b/src/ts/components/errormessageoverlay.ts @@ -19,6 +19,8 @@ export interface ErrorMessageMap { /** * Configuration interface for the {@link ErrorMessageOverlay}. + * + * @category Configs */ export interface ErrorMessageOverlayConfig extends ContainerConfig { /** @@ -80,6 +82,8 @@ export interface ErrorMessageOverlayConfig extends ContainerConfig { /** * Overlays the player and displays error messages. + * + * @category Components */ export class ErrorMessageOverlay extends Container { diff --git a/src/ts/components/fullscreentogglebutton.ts b/src/ts/components/fullscreentogglebutton.ts index eac83d1e3..968344dc5 100644 --- a/src/ts/components/fullscreentogglebutton.ts +++ b/src/ts/components/fullscreentogglebutton.ts @@ -5,6 +5,8 @@ import { i18n } from '../localization/i18n'; /** * A button that toggles the player between windowed and fullscreen view. + * + * @category Buttons */ export class FullscreenToggleButton extends ToggleButton { diff --git a/src/ts/components/hugeplaybacktogglebutton.ts b/src/ts/components/hugeplaybacktogglebutton.ts index 0bd4ae69c..afd4ea308 100644 --- a/src/ts/components/hugeplaybacktogglebutton.ts +++ b/src/ts/components/hugeplaybacktogglebutton.ts @@ -6,6 +6,8 @@ import { i18n } from '../localization/i18n'; /** * A button that overlays the video and toggles between playback and pause. + * + * @category Buttons */ export class HugePlaybackToggleButton extends PlaybackToggleButton { diff --git a/src/ts/components/hugereplaybutton.ts b/src/ts/components/hugereplaybutton.ts index d46cef5d2..658d0c873 100644 --- a/src/ts/components/hugereplaybutton.ts +++ b/src/ts/components/hugereplaybutton.ts @@ -6,6 +6,8 @@ import { i18n } from '../localization/i18n'; /** * A button to play/replay a video. + * + * @category Buttons */ export class HugeReplayButton extends Button { diff --git a/src/ts/components/itemselectionlist.ts b/src/ts/components/itemselectionlist.ts index eb41c650e..fbacff552 100644 --- a/src/ts/components/itemselectionlist.ts +++ b/src/ts/components/itemselectionlist.ts @@ -2,6 +2,9 @@ import {ListSelector, ListSelectorConfig} from './listselector'; import {DOM} from '../dom'; import { i18n } from '../localization/i18n'; +/** + * @category Components + */ export class ItemSelectionList extends ListSelector { private static readonly CLASS_SELECTED = 'selected'; @@ -25,7 +28,7 @@ export class ItemSelectionList extends ListSelector { let listElement = new DOM('ul', { 'id': this.config.id, 'class': this.getCssClasses(), - }); + }, this); this.listElement = listElement; this.updateDomItems(); diff --git a/src/ts/components/label.ts b/src/ts/components/label.ts index bbd403494..3cc8062b0 100644 --- a/src/ts/components/label.ts +++ b/src/ts/components/label.ts @@ -5,6 +5,8 @@ import { LocalizableText, i18n } from '../localization/i18n'; /** * Configuration interface for a {@link Label} component. + * + * @category Configs */ export interface LabelConfig extends ComponentConfig { /** @@ -25,6 +27,8 @@ export interface LabelConfig extends ComponentConfig { * * ...some text... * + * + * @category Components */ export class Label extends Component { @@ -51,7 +55,7 @@ export class Label extends Component { 'id': this.config.id, 'for': this.config.for, 'class': this.getCssClasses(), - }).html(i18n.performLocalization(this.text)); + }, this).html(i18n.performLocalization(this.text)); labelElement.on('click', () => { this.onClickEvent(); diff --git a/src/ts/components/listbox.ts b/src/ts/components/listbox.ts index ea3503bda..c60248225 100644 --- a/src/ts/components/listbox.ts +++ b/src/ts/components/listbox.ts @@ -15,6 +15,8 @@ import { ArrayUtils } from '../arrayutils'; * ... *
+ * + * @category Components */ // TODO: change ListSelector to extends container in v4 to improve usage of ListBox. // Currently we are creating the dom element of the list box with child elements manually here. @@ -44,7 +46,7 @@ export class ListBox extends ListSelector { let listBoxElement = new DOM('div', { 'id': this.config.id, 'class': this.getCssClasses(), - }); + }, this); this.listBoxElement = listBoxElement; this.createListBoxDomItems(); diff --git a/src/ts/components/listselector.ts b/src/ts/components/listselector.ts index 615c5ccb7..c8157295a 100644 --- a/src/ts/components/listselector.ts +++ b/src/ts/components/listselector.ts @@ -45,6 +45,8 @@ export interface ListItemLabelTranslator { /** * Configuration interface for a {@link ListSelector}. + * + * @category Configs */ export interface ListSelectorConfig extends ComponentConfig { items?: ListItem[]; diff --git a/src/ts/components/metadatalabel.ts b/src/ts/components/metadatalabel.ts index 2ca8def22..cdb5cd8e0 100644 --- a/src/ts/components/metadatalabel.ts +++ b/src/ts/components/metadatalabel.ts @@ -18,6 +18,8 @@ export enum MetadataLabelContent { /** * Configuration interface for {@link MetadataLabel}. + * + * @category Configs */ export interface MetadataLabelConfig extends LabelConfig { /** @@ -28,6 +30,8 @@ export interface MetadataLabelConfig extends LabelConfig { /** * A label that can be configured to display certain metadata. + * + * @category Labels */ export class MetadataLabel extends Label { diff --git a/src/ts/components/pictureinpicturetogglebutton.ts b/src/ts/components/pictureinpicturetogglebutton.ts index 676bbabd7..9644eb9a0 100644 --- a/src/ts/components/pictureinpicturetogglebutton.ts +++ b/src/ts/components/pictureinpicturetogglebutton.ts @@ -5,6 +5,8 @@ import { i18n } from '../localization/i18n'; /** * A button that toggles Apple macOS picture-in-picture mode. + * + * @category Buttons */ export class PictureInPictureToggleButton extends ToggleButton { diff --git a/src/ts/components/playbackspeedselectbox.ts b/src/ts/components/playbackspeedselectbox.ts index c1d6c18f1..d4afada23 100644 --- a/src/ts/components/playbackspeedselectbox.ts +++ b/src/ts/components/playbackspeedselectbox.ts @@ -6,6 +6,8 @@ import { i18n } from '../localization/i18n'; /** * A select box providing a selection of different playback speeds. + * + * @category Components */ export class PlaybackSpeedSelectBox extends SelectBox { protected defaultPlaybackSpeeds: number[]; diff --git a/src/ts/components/playbacktimelabel.ts b/src/ts/components/playbacktimelabel.ts index 204f0a0b0..2b672971b 100644 --- a/src/ts/components/playbacktimelabel.ts +++ b/src/ts/components/playbacktimelabel.ts @@ -26,6 +26,9 @@ export enum PlaybackTimeLabelMode { RemainingTime, } +/** + * @category Configs + */ export interface PlaybackTimeLabelConfig extends LabelConfig { /** * The type of which time should be displayed in the label. @@ -41,6 +44,8 @@ export interface PlaybackTimeLabelConfig extends LabelConfig { /** * A label that display the current playback time and the total time through {@link PlaybackTimeLabel#setTime setTime} * or any string through {@link PlaybackTimeLabel#setText setText}. + * + * @category Labels */ export class PlaybackTimeLabel extends Label { diff --git a/src/ts/components/playbacktogglebutton.ts b/src/ts/components/playbacktogglebutton.ts index eabc2b6c3..06837a1aa 100644 --- a/src/ts/components/playbacktogglebutton.ts +++ b/src/ts/components/playbacktogglebutton.ts @@ -4,6 +4,9 @@ import {PlayerUtils} from '../playerutils'; import { PlayerAPI, WarningEvent } from 'bitmovin-player'; import { i18n } from '../localization/i18n'; +/** + * @category Configs + */ export interface PlaybackToggleButtonConfig extends ToggleButtonConfig { /** * Specify whether the player should be set to enter fullscreen by clicking on the playback toggle button @@ -15,6 +18,8 @@ export interface PlaybackToggleButtonConfig extends ToggleButtonConfig { /** * A button that toggles between playback and pause. + * + * @category Buttons */ export class PlaybackToggleButton extends ToggleButton { diff --git a/src/ts/components/playbacktoggleoverlay.ts b/src/ts/components/playbacktoggleoverlay.ts index d0ed6b1bc..d72ca9346 100644 --- a/src/ts/components/playbacktoggleoverlay.ts +++ b/src/ts/components/playbacktoggleoverlay.ts @@ -1,6 +1,9 @@ import {Container, ContainerConfig} from './container'; import {HugePlaybackToggleButton} from './hugeplaybacktogglebutton'; +/** + * @category Configs + */ export interface PlaybackToggleOverlayConfig extends ContainerConfig { /** * Specify whether the player should be set to enter fullscreen by clicking on the playback toggle button @@ -12,6 +15,8 @@ export interface PlaybackToggleOverlayConfig extends ContainerConfig { /** * Overlays the player and displays error messages. + * + * @category Components */ export class PlaybackToggleOverlay extends Container { diff --git a/src/ts/components/quickseekbutton.ts b/src/ts/components/quickseekbutton.ts new file mode 100644 index 000000000..841a7441f --- /dev/null +++ b/src/ts/components/quickseekbutton.ts @@ -0,0 +1,135 @@ +import { Button, ButtonConfig } from './button'; +import { i18n } from '../localization/i18n'; +import { PlayerAPI, SeekEvent, TimeShiftEvent } from 'bitmovin-player'; +import { UIInstanceManager } from '../uimanager'; +import { PlayerUtils } from '../playerutils'; + +/** + * @category Configs + */ +export interface QuickSeekButtonConfig extends ButtonConfig { + /** + * Specify how many seconds the player should seek forward/backwards in the stream. + * Negative values mean a backwards seek, positive values mean a forward seek. + * Default is -10. + */ + seekSeconds?: number; +} + +/** + * @category Buttons + */ +export class QuickSeekButton extends Button { + private currentSeekTarget: number | null; + private player: PlayerAPI; + + constructor(config: QuickSeekButtonConfig = {}) { + super(config); + this.currentSeekTarget = null; + + this.config = this.mergeConfig( + config, + { + seekSeconds: -10, + cssClass: 'ui-quickseekbutton', + }, + this.config, + ); + + const seekDirection = this.config.seekSeconds < 0 ? 'rewind' : 'forward'; + + this.config.text = this.config.text || i18n.getLocalizer(`quickseek.${seekDirection}`); + this.config.ariaLabel = this.config.ariaLabel || i18n.getLocalizer(`quickseek.${seekDirection}`); + + this.getDomElement().data(this.prefixCss('seek-direction'), seekDirection); + } + + configure(player: PlayerAPI, uimanager: UIInstanceManager): void { + super.configure(player, uimanager); + this.player = player; + + let isLive: boolean; + let hasTimeShift: boolean; + + const switchVisibility = (isLive: boolean, hasTimeShift: boolean) => { + if (isLive && !hasTimeShift) { + this.hide(); + } else { + this.show(); + } + }; + + const timeShiftDetector = new PlayerUtils.TimeShiftAvailabilityDetector(player); + timeShiftDetector.onTimeShiftAvailabilityChanged.subscribe( + (sender, args: PlayerUtils.TimeShiftAvailabilityChangedArgs) => { + hasTimeShift = args.timeShiftAvailable; + switchVisibility(isLive, hasTimeShift); + }, + ); + + let liveStreamDetector = new PlayerUtils.LiveStreamDetector(player, uimanager); + liveStreamDetector.onLiveChanged.subscribe((sender, args: PlayerUtils.LiveStreamDetectorEventArgs) => { + isLive = args.live; + switchVisibility(isLive, hasTimeShift); + }); + + // Initial detection + timeShiftDetector.detect(); + liveStreamDetector.detect(); + + this.onClick.subscribe(() => { + if (isLive && !hasTimeShift) { + // If no DVR window is available, the button should be hidden anyway, so this is to be absolutely sure + return; + } + + if (isLive && this.config.seekSeconds > 0 && player.getTimeShift() === 0) { + // Don't do anything if the player is already on the live edge + return; + } + + const currentPosition = + this.currentSeekTarget !== null + ? this.currentSeekTarget + : isLive + ? player.getTimeShift() + : player.getCurrentTime(); + + const newSeekTime = currentPosition + this.config.seekSeconds; + + if (isLive) { + const clampedValue = PlayerUtils.clampValueToRange(newSeekTime, player.getMaxTimeShift(), 0); + player.timeShift(clampedValue); + } else { + const clampedValue = PlayerUtils.clampValueToRange(newSeekTime, 0, player.getDuration()); + player.seek(clampedValue); + } + }); + + this.player.on(this.player.exports.PlayerEvent.Seek, this.onSeek); + this.player.on(this.player.exports.PlayerEvent.Seeked, this.onSeekedOrTimeShifted); + this.player.on(this.player.exports.PlayerEvent.TimeShift, this.onTimeShift); + this.player.on(this.player.exports.PlayerEvent.TimeShifted, this.onSeekedOrTimeShifted); + } + + private onSeek = (event: SeekEvent): void => { + this.currentSeekTarget = event.seekTarget; + }; + + private onSeekedOrTimeShifted = () => { + this.currentSeekTarget = null; + }; + + private onTimeShift = (event: TimeShiftEvent): void => { + this.currentSeekTarget = this.player.getTimeShift() + (event.target - event.position); + } + + release(): void { + this.player.off(this.player.exports.PlayerEvent.Seek, this.onSeek); + this.player.off(this.player.exports.PlayerEvent.Seeked, this.onSeekedOrTimeShifted); + this.player.off(this.player.exports.PlayerEvent.TimeShift, this.onTimeShift); + this.player.off(this.player.exports.PlayerEvent.TimeShifted, this.onSeekedOrTimeShifted); + this.currentSeekTarget = null; + this.player = null; + } +} diff --git a/src/ts/components/recommendationoverlay.ts b/src/ts/components/recommendationoverlay.ts index 2800cb87c..86d2fdce5 100644 --- a/src/ts/components/recommendationoverlay.ts +++ b/src/ts/components/recommendationoverlay.ts @@ -9,6 +9,8 @@ import { PlayerAPI } from 'bitmovin-player'; /** * Overlays the player and displays recommended videos. + * + * @category Containers */ export class RecommendationOverlay extends Container { @@ -106,7 +108,7 @@ class RecommendationItem extends Component { 'id': this.config.id, 'class': this.getCssClasses(), 'href': config.url, - }).css({ 'background-image': `url(${config.thumbnail})` }); + }, this).css({ 'background-image': `url(${config.thumbnail})` }); let bgElement = new DOM('div', { 'class': this.prefixCss('background'), diff --git a/src/ts/components/replaybutton.ts b/src/ts/components/replaybutton.ts index 841c99e01..6278ed1f3 100644 --- a/src/ts/components/replaybutton.ts +++ b/src/ts/components/replaybutton.ts @@ -7,6 +7,8 @@ import LiveStreamDetectorEventArgs = PlayerUtils.LiveStreamDetectorEventArgs; /** * A button to play/replay a video. + * + * @category Buttons */ export class ReplayButton extends Button { @@ -16,6 +18,7 @@ export class ReplayButton extends Button { this.config = this.mergeConfig(config, { cssClass: 'ui-replaybutton', text: i18n.getLocalizer('replay'), + ariaLabel: i18n.getLocalizer('replay'), }, this.config); } diff --git a/src/ts/components/seekbar.ts b/src/ts/components/seekbar.ts index 786a8c38a..c06cc0137 100644 --- a/src/ts/components/seekbar.ts +++ b/src/ts/components/seekbar.ts @@ -19,6 +19,8 @@ import { getMinBufferLevel } from './seekbarbufferlevel'; /** * Configuration interface for the {@link SeekBar} component. + * + * @category Configs */ export interface SeekBarConfig extends ComponentConfig { /** @@ -70,6 +72,9 @@ export interface SeekPreviewEventArgs extends SeekPreviewArgs { scrubbing: boolean; } +/** + * @category Components + */ export interface SeekBarMarker { marker: TimelineMarker; position: number; @@ -85,6 +90,8 @@ export interface SeekBarMarker { * - the playback position, i.e. the position in the media at which the player current playback pointer is positioned * - the buffer position, which usually is the playback position plus the time span that is already buffered ahead * - the seek position, used to preview to where in the timeline a seek will jump to + * + * @category Components */ export class SeekBar extends Component { @@ -647,7 +654,7 @@ export class SeekBar extends Component { 'role': 'slider', 'aria-label': i18n.performLocalization(this.config.ariaLabel), 'tabindex': this.config.tabIndex.toString(), - }); + }, this); let seekBar = new DOM('div', { 'class': this.prefixCss('seekbar'), diff --git a/src/ts/components/seekbarcontroller.ts b/src/ts/components/seekbarcontroller.ts index 8406854b5..7f43cd5b6 100644 --- a/src/ts/components/seekbarcontroller.ts +++ b/src/ts/components/seekbarcontroller.ts @@ -33,6 +33,9 @@ const coerceValueIntoRange = ( } }; +/** + * @category Utils + */ export class SeekBarController { protected keyStepIncrements: KeyStepIncrementsConfig; protected player: PlayerAPI; diff --git a/src/ts/components/seekbarlabel.ts b/src/ts/components/seekbarlabel.ts index 743f5946d..eefb26beb 100644 --- a/src/ts/components/seekbarlabel.ts +++ b/src/ts/components/seekbarlabel.ts @@ -11,6 +11,8 @@ import { PlayerUtils } from '../playerutils'; /** * Configuration interface for a {@link SeekBarLabel}. + * + * @category Configs */ export interface SeekBarLabelConfig extends ContainerConfig { // nothing yet @@ -18,6 +20,8 @@ export interface SeekBarLabelConfig extends ContainerConfig { /** * A label for a {@link SeekBar} that can display the seek target time, a thumbnail, and title (e.g. chapter title). + * + * @category Components */ export class SeekBarLabel extends Container { diff --git a/src/ts/components/selectbox.ts b/src/ts/components/selectbox.ts index e5a1a86e0..54780310b 100644 --- a/src/ts/components/selectbox.ts +++ b/src/ts/components/selectbox.ts @@ -1,6 +1,40 @@ -import {ListSelector, ListSelectorConfig} from './listselector'; -import {DOM} from '../dom'; -import { i18n, LocalizableText } from '../localization/i18n'; +import { ListSelector, ListSelectorConfig } from './listselector'; +import { DOM } from '../dom'; +import { i18n } from '../localization/i18n'; +import { PlayerAPI } from 'bitmovin-player'; +import { UIInstanceManager } from '../uimanager'; +import { UIContainer } from './uicontainer'; +import { PlayerUtils } from '../playerutils'; +import { ViewMode } from './component'; + +const DocumentDropdownClosedEvents = [ + 'mousemove', + 'mouseenter', + 'mouseleave', + 'touchstart', + 'touchmove', + 'touchend', + 'pointermove', + 'click', + 'keydown', + 'keypress', + 'keyup', + 'blur', +]; + +const SelectDropdownClosedEvents = [ + 'change', + 'keyup', + 'mouseup', +]; + +const DropdownOpenedEvents: [string, (event: Event) => boolean][] = [ + ['click', () => true], + ['keydown', (event: KeyboardEvent) => [' ', 'ArrowUp', 'ArrowDown'].includes(event.key)], + ['mousedown', () => true], +]; + +const Timeout = 100; /** * A simple select box providing the possibility to select a single item out of a list of available items. @@ -12,11 +46,16 @@ import { i18n, LocalizableText } from '../localization/i18n'; * ... * * + * + * @category Components */ export class SelectBox extends ListSelector { - - private selectElement: DOM; + private selectElement: DOM | undefined; + private dropdownCloseListenerTimeoutId = 0; + private removeDropdownCloseListeners = () => {}; + private uiContainer: UIContainer | undefined; + private removeDropdownOpenedListeners = () => {}; constructor(config: ListSelectorConfig = {}) { super(config); @@ -27,24 +66,42 @@ export class SelectBox extends ListSelector { } protected toDomElement(): DOM { - let selectElement = new DOM('select', { + this.selectElement = new DOM('select', { 'id': this.config.id, 'class': this.getCssClasses(), 'aria-label': i18n.performLocalization(this.config.ariaLabel), - }); + }, this); - this.selectElement = selectElement; + this.onDisabled.subscribe(this.closeDropdown); + this.onHide.subscribe(this.closeDropdown); + this.addDropdownOpenedListeners(); this.updateDomItems(); - selectElement.on('change', () => { - let value = selectElement.val(); - this.onItemSelectedEvent(value, false); - }); + this.selectElement.on('change', this.onChange); - return selectElement; + return this.selectElement; + } + + configure(player: PlayerAPI, uimanager: UIInstanceManager) { + super.configure(player, uimanager); + this.uiContainer = uimanager.getUI(); + this.uiContainer?.onPlayerStateChange().subscribe(this.onPlayerStateChange); + } + + private readonly onChange = () => { + let value = this.selectElement.val(); + this.onItemSelectedEvent(value, false); + }; + + private getSelectElement() { + return this.selectElement?.get()?.[0]; } protected updateDomItems(selectedValue: string = null) { + if (this.selectElement === undefined) { + return; + } + // Delete all children this.selectElement.empty(); @@ -78,4 +135,79 @@ export class SelectBox extends ListSelector { this.updateDomItems(value); } } + + public readonly closeDropdown = () => { + const select = this.getSelectElement(); + + if (select === undefined) { + return; + } + + select.blur(); + }; + + private readonly onPlayerStateChange = (_: UIContainer, state: PlayerUtils.PlayerState) => { + if ([PlayerUtils.PlayerState.Idle, PlayerUtils.PlayerState.Finished].includes(state)) { + this.closeDropdown(); + } + }; + + private onDropdownOpened = () => { + clearTimeout(this.dropdownCloseListenerTimeoutId); + + this.dropdownCloseListenerTimeoutId = window.setTimeout(() => this.addDropdownCloseListeners(), Timeout); + this.onViewModeChangedEvent(ViewMode.Persistent); + }; + + private onDropdownClosed = () => { + clearTimeout(this.dropdownCloseListenerTimeoutId); + + this.removeDropdownCloseListeners(); + this.onViewModeChangedEvent(ViewMode.Temporary); + }; + + private addDropdownCloseListeners() { + this.removeDropdownCloseListeners(); + + clearTimeout(this.dropdownCloseListenerTimeoutId); + + DocumentDropdownClosedEvents.forEach(event => document.addEventListener(event, this.onDropdownClosed, true)); + SelectDropdownClosedEvents.forEach(event => this.selectElement.on(event, this.onDropdownClosed, true)); + + this.removeDropdownCloseListeners = () => { + DocumentDropdownClosedEvents.forEach(event => document.removeEventListener(event, this.onDropdownClosed, true)); + SelectDropdownClosedEvents.forEach(event => this.selectElement.off(event, this.onDropdownClosed, true)); + }; + } + + private addDropdownOpenedListeners() { + const removeListenerFunctions: (() => void)[] = []; + + this.removeDropdownOpenedListeners(); + + for (const [event, filter] of DropdownOpenedEvents) { + const listener = (event: Event) => { + if (filter(event)) { + this.onDropdownOpened(); + } + }; + + removeListenerFunctions.push(() => this.selectElement.off(event, listener, true)); + this.selectElement.on(event, listener, true); + } + + this.removeDropdownOpenedListeners = () => { + for (const remove of removeListenerFunctions) { + remove(); + } + }; + } + + release() { + super.release(); + + this.removeDropdownCloseListeners(); + this.removeDropdownOpenedListeners(); + clearTimeout(this.dropdownCloseListenerTimeoutId); + } } diff --git a/src/ts/components/settingspanel.ts b/src/ts/components/settingspanel.ts index c09dbb990..fd0183599 100644 --- a/src/ts/components/settingspanel.ts +++ b/src/ts/components/settingspanel.ts @@ -10,6 +10,8 @@ import { Component, ComponentConfig } from './component'; /** * Configuration interface for a {@link SettingsPanel}. + * + * @category Configs */ export interface SettingsPanelConfig extends ContainerConfig { /** @@ -53,6 +55,8 @@ enum NavigationDirection { * settingsPanel.addComponent(secondSettingsPanelPage); * * For an example how to navigate between pages @see SettingsPanelPageNavigatorButton + * + * @category Components */ export class SettingsPanel extends Container { @@ -86,6 +90,7 @@ export class SettingsPanel extends Container { let config = this.getConfig(); uimanager.onControlsHide.subscribe(() => this.hideHoveredSelectBoxes()); + uimanager.onComponentViewModeChanged.subscribe((_, { mode }) => this.trackComponentViewMode(mode)); if (config.hideDelay > -1) { this.hideTimeout = new Timeout(config.hideDelay, () => { @@ -162,7 +167,7 @@ export class SettingsPanel extends Container { * Use {@link popSettingsPanelPage} to navigate backwards. * * Results in no-op if the target page is the current page. - * @params page + * @param targetPage */ setActivePage(targetPage: SettingsPanelPage): void { if (targetPage === this.getActivePage()) { @@ -245,6 +250,14 @@ export class SettingsPanel extends Container { super.addComponent(component); } + protected suspendHideTimeout() { + this.hideTimeout.suspend(); + } + + protected resumeHideTimeout() { + this.hideTimeout.resume(true); + } + private updateActivePageClass(): void { this.getPages().forEach((page: SettingsPanelPage) => { if (page === this.activePage) { @@ -358,36 +371,15 @@ export class SettingsPanel extends Container { } /** - * Hack for IE + Firefox + * Workaround for IE, Firefox and Safari * when the settings panel fades out while an item of a select box is still hovered, the select box will not fade out * while the settings panel does. This would leave a floating select box, which is just weird */ private hideHoveredSelectBoxes(): void { - this.getComputedItems().forEach((item: SettingsPanelItem) => { - if (item.isActive() && (item as any).setting instanceof SelectBox) { - const selectBox = (item as any).setting as SelectBox; - const oldDisplay = selectBox.getDomElement().css('display'); - if (oldDisplay === 'none') { - // if oldDisplay is already 'none', no need to set to 'none' again. It could lead to race condition - // wherein the display is irreversibly set to 'none' when browser tab/window is not active because - // requestAnimationFrame is either delayed or paused in some browsers in inactive state - return; - } - - // updating the display to none marks the select-box as inactive, so it will be hidden with the rest - // we just have to make sure to reset this as soon as possible - selectBox.getDomElement().css('display', 'none'); - if (window.requestAnimationFrame) { - requestAnimationFrame(() => { - selectBox.getDomElement().css('display', oldDisplay); - }); - } else { - // IE9 has no requestAnimationFrame, set the value directly. It has no optimization about ignoring DOM-changes - // between animationFrames - selectBox.getDomElement().css('display', oldDisplay); - } - } - }); + this.getComputedItems() + .map(item => item['setting']) + .filter(component => component instanceof SelectBox) + .forEach((selectBox: SelectBox) => selectBox.closeDropdown()); } // collect all items from all pages (see hideHoveredSelectBoxes) diff --git a/src/ts/components/settingspanelitem.ts b/src/ts/components/settingspanelitem.ts index 57c02e643..5d33f5318 100644 --- a/src/ts/components/settingspanelitem.ts +++ b/src/ts/components/settingspanelitem.ts @@ -15,6 +15,8 @@ import { LocalizableText } from '../localization/i18n'; * An item for a {@link SettingsPanelPage}, * Containing an optional {@link Label} and a component that configures a setting. * If the components is a {@link SelectBox} it will handle the logic of displaying it or not + * + * @category Components */ export class SettingsPanelItem extends Container { diff --git a/src/ts/components/settingspanelpage.ts b/src/ts/components/settingspanelpage.ts index fce244f2d..8e8f089ff 100644 --- a/src/ts/components/settingspanelpage.ts +++ b/src/ts/components/settingspanelpage.ts @@ -7,6 +7,8 @@ import { BrowserUtils } from '../browserutils'; /** * A panel containing a list of {@link SettingsPanelItem items} that represent labelled settings. + * + * @category Components */ export class SettingsPanelPage extends Container { diff --git a/src/ts/components/settingspanelpagebackbutton.ts b/src/ts/components/settingspanelpagebackbutton.ts index e0a306933..e02b4be31 100644 --- a/src/ts/components/settingspanelpagebackbutton.ts +++ b/src/ts/components/settingspanelpagebackbutton.ts @@ -2,6 +2,9 @@ import {UIInstanceManager} from '../uimanager'; import {SettingsPanelPageNavigatorButton, SettingsPanelPageNavigatorConfig} from './settingspanelpagenavigatorbutton'; import { PlayerAPI } from 'bitmovin-player'; +/** + * @category Buttons + */ export class SettingsPanelPageBackButton extends SettingsPanelPageNavigatorButton { constructor(config: SettingsPanelPageNavigatorConfig) { diff --git a/src/ts/components/settingspanelpagenavigatorbutton.ts b/src/ts/components/settingspanelpagenavigatorbutton.ts index 1208eb69e..a28239255 100644 --- a/src/ts/components/settingspanelpagenavigatorbutton.ts +++ b/src/ts/components/settingspanelpagenavigatorbutton.ts @@ -6,6 +6,8 @@ import { UIInstanceManager } from '../uimanager'; /** * Configuration interface for a {@link SettingsPanelPageNavigatorButton} + * + * @category Configs */ export interface SettingsPanelPageNavigatorConfig extends ButtonConfig { /** @@ -36,6 +38,8 @@ export interface SettingsPanelPageNavigatorConfig extends ButtonConfig { * settingsPanelPage.addComponent(settingPanelNavigationButton); * * Don't forget to add the settingPanelNavigationButton to the settingsPanelPage. + * + * @category Buttons */ export class SettingsPanelPageNavigatorButton extends Button { private readonly container: SettingsPanel; diff --git a/src/ts/components/settingspanelpageopenbutton.ts b/src/ts/components/settingspanelpageopenbutton.ts index 6f6cd2bdc..edc7ed545 100644 --- a/src/ts/components/settingspanelpageopenbutton.ts +++ b/src/ts/components/settingspanelpageopenbutton.ts @@ -3,6 +3,9 @@ import {SettingsPanelPageNavigatorButton, SettingsPanelPageNavigatorConfig} from import { PlayerAPI } from 'bitmovin-player'; import { i18n } from '../localization/i18n'; +/** + * @category Buttons + */ export class SettingsPanelPageOpenButton extends SettingsPanelPageNavigatorButton { constructor(config: SettingsPanelPageNavigatorConfig) { super(config); diff --git a/src/ts/components/settingstogglebutton.ts b/src/ts/components/settingstogglebutton.ts index 5626f6d37..bfb188182 100644 --- a/src/ts/components/settingstogglebutton.ts +++ b/src/ts/components/settingstogglebutton.ts @@ -8,6 +8,8 @@ import { i18n } from '../localization/i18n'; /** * Configuration interface for the {@link SettingsToggleButton}. + * + * @category Configs */ export interface SettingsToggleButtonConfig extends ToggleButtonConfig { /** @@ -24,6 +26,8 @@ export interface SettingsToggleButtonConfig extends ToggleButtonConfig { /** * A button that toggles visibility of a settings panel. + * + * @category Buttons */ export class SettingsToggleButton extends ToggleButton { diff --git a/src/ts/components/spacer.ts b/src/ts/components/spacer.ts index b0bb6cbe6..b565f90cf 100644 --- a/src/ts/components/spacer.ts +++ b/src/ts/components/spacer.ts @@ -2,6 +2,8 @@ import {Component, ComponentConfig} from './component'; /** * A dummy component that just reserves some space and does nothing else. + * + * @category Components */ export class Spacer extends Component { diff --git a/src/ts/components/subtitlelistbox.ts b/src/ts/components/subtitlelistbox.ts index cc1e8cea7..1aab133a6 100644 --- a/src/ts/components/subtitlelistbox.ts +++ b/src/ts/components/subtitlelistbox.ts @@ -5,6 +5,8 @@ import { PlayerAPI } from 'bitmovin-player'; /** * A element that is similar to a select box where the user can select a subtitle + * + * @category Components */ export class SubtitleListBox extends ListBox { diff --git a/src/ts/components/subtitleoverlay.ts b/src/ts/components/subtitleoverlay.ts index ba5b93124..14ed7ee64 100644 --- a/src/ts/components/subtitleoverlay.ts +++ b/src/ts/components/subtitleoverlay.ts @@ -19,6 +19,8 @@ interface SubtitleCropDetectionResult { /** * Overlays the player to display subtitles. + * + * @category Components */ export class SubtitleOverlay extends Container { @@ -129,7 +131,6 @@ export class SubtitleOverlay extends Container { }; player.on(player.exports.PlayerEvent.AudioChanged, subtitleClearHandler); - player.on(player.exports.PlayerEvent.SubtitleEnabled, subtitleClearHandler); player.on(player.exports.PlayerEvent.SubtitleDisabled, subtitleClearHandler); player.on(player.exports.PlayerEvent.Seeked, clearInactiveCues); player.on(player.exports.PlayerEvent.TimeShifted, clearInactiveCues); diff --git a/src/ts/components/subtitleselectbox.ts b/src/ts/components/subtitleselectbox.ts index e1a75b4f0..e2d20df87 100644 --- a/src/ts/components/subtitleselectbox.ts +++ b/src/ts/components/subtitleselectbox.ts @@ -7,6 +7,8 @@ import { i18n } from '../localization/i18n'; /** * A select box providing a selection between available subtitle and caption tracks. + * + * @category Components */ export class SubtitleSelectBox extends SelectBox { diff --git a/src/ts/components/subtitlesettings/backgroundcolorselectbox.ts b/src/ts/components/subtitlesettings/backgroundcolorselectbox.ts index 58002609d..47cf64388 100644 --- a/src/ts/components/subtitlesettings/backgroundcolorselectbox.ts +++ b/src/ts/components/subtitlesettings/backgroundcolorselectbox.ts @@ -5,6 +5,8 @@ import { i18n } from '../../localization/i18n'; /** * A select box providing a selection of different background colors. + * + * @category Components */ export class BackgroundColorSelectBox extends SubtitleSettingSelectBox { diff --git a/src/ts/components/subtitlesettings/backgroundopacityselectbox.ts b/src/ts/components/subtitlesettings/backgroundopacityselectbox.ts index cd70635f7..3d8970799 100644 --- a/src/ts/components/subtitlesettings/backgroundopacityselectbox.ts +++ b/src/ts/components/subtitlesettings/backgroundopacityselectbox.ts @@ -5,6 +5,8 @@ import { i18n } from '../../localization/i18n'; /** * A select box providing a selection of different background opacity. + * + * @category Components */ export class BackgroundOpacitySelectBox extends SubtitleSettingSelectBox { diff --git a/src/ts/components/subtitlesettings/characteredgeselectbox.ts b/src/ts/components/subtitlesettings/characteredgeselectbox.ts index d93513735..139a53e96 100644 --- a/src/ts/components/subtitlesettings/characteredgeselectbox.ts +++ b/src/ts/components/subtitlesettings/characteredgeselectbox.ts @@ -5,6 +5,8 @@ import { i18n } from '../../localization/i18n'; /** * A select box providing a selection of different character edge. + * + * @category Components */ export class CharacterEdgeSelectBox extends SubtitleSettingSelectBox { diff --git a/src/ts/components/subtitlesettings/fontcolorselectbox.ts b/src/ts/components/subtitlesettings/fontcolorselectbox.ts index 8302f390b..54c0423a0 100644 --- a/src/ts/components/subtitlesettings/fontcolorselectbox.ts +++ b/src/ts/components/subtitlesettings/fontcolorselectbox.ts @@ -5,6 +5,8 @@ import { i18n } from '../../localization/i18n'; /** * A select box providing a selection of different font colors. + * + * @category Components */ export class FontColorSelectBox extends SubtitleSettingSelectBox { diff --git a/src/ts/components/subtitlesettings/fontfamilyselectbox.ts b/src/ts/components/subtitlesettings/fontfamilyselectbox.ts index 024dea369..e389b9ed6 100644 --- a/src/ts/components/subtitlesettings/fontfamilyselectbox.ts +++ b/src/ts/components/subtitlesettings/fontfamilyselectbox.ts @@ -5,6 +5,8 @@ import { i18n } from '../../localization/i18n'; /** * A select box providing a selection of different font family. + * + * @category Components */ export class FontFamilySelectBox extends SubtitleSettingSelectBox { diff --git a/src/ts/components/subtitlesettings/fontopacityselectbox.ts b/src/ts/components/subtitlesettings/fontopacityselectbox.ts index 018225c96..df3032586 100644 --- a/src/ts/components/subtitlesettings/fontopacityselectbox.ts +++ b/src/ts/components/subtitlesettings/fontopacityselectbox.ts @@ -5,6 +5,8 @@ import { i18n } from '../../localization/i18n'; /** * A select box providing a selection of different font colors. + * + * @category Components */ export class FontOpacitySelectBox extends SubtitleSettingSelectBox { diff --git a/src/ts/components/subtitlesettings/fontsizeselectbox.ts b/src/ts/components/subtitlesettings/fontsizeselectbox.ts index a795c1574..dc98dd766 100644 --- a/src/ts/components/subtitlesettings/fontsizeselectbox.ts +++ b/src/ts/components/subtitlesettings/fontsizeselectbox.ts @@ -5,6 +5,8 @@ import { i18n } from '../../localization/i18n'; /** * A select box providing a selection of different font colors. + * + * @category Components */ export class FontSizeSelectBox extends SubtitleSettingSelectBox { diff --git a/src/ts/components/subtitlesettings/subtitlesettingselectbox.ts b/src/ts/components/subtitlesettings/subtitlesettingselectbox.ts index 91dbf7500..e6448d56f 100644 --- a/src/ts/components/subtitlesettings/subtitlesettingselectbox.ts +++ b/src/ts/components/subtitlesettings/subtitlesettingselectbox.ts @@ -5,12 +5,17 @@ import {SubtitleSettingsManager} from './subtitlesettingsmanager'; import { PlayerAPI } from 'bitmovin-player'; import { UIInstanceManager } from '../../uimanager'; +/** + * @category Configs + */ export interface SubtitleSettingSelectBoxConfig extends ListSelectorConfig { overlay: SubtitleOverlay; } /** * Base class for all subtitles settings select box + * + * @category Components **/ export class SubtitleSettingSelectBox extends SelectBox { diff --git a/src/ts/components/subtitlesettings/subtitlesettingslabel.ts b/src/ts/components/subtitlesettings/subtitlesettingslabel.ts index ea6aa209a..723db03c5 100644 --- a/src/ts/components/subtitlesettings/subtitlesettingslabel.ts +++ b/src/ts/components/subtitlesettings/subtitlesettingslabel.ts @@ -4,10 +4,16 @@ import {DOM} from '../../dom'; import {SettingsPanelPageOpenButton} from '../settingspanelpageopenbutton'; import { LocalizableText, i18n } from '../../localization/i18n'; +/** + * @category Configs + */ export interface SubtitleSettingsLabelConfig extends LabelConfig { opener: SettingsPanelPageOpenButton; } +/** + * @category Components + */ export class SubtitleSettingsLabel extends Container { private opener: SettingsPanelPageOpenButton; @@ -36,7 +42,7 @@ export class SubtitleSettingsLabel extends Container { 'id': this.config.id, 'class': this.getCssClasses(), 'for': this.for, - }).append( + }, this).append( new DOM('span', {}).html(i18n.performLocalization(this.text)), this.opener.getDomElement(), ); diff --git a/src/ts/components/subtitlesettings/subtitlesettingsmanager.ts b/src/ts/components/subtitlesettings/subtitlesettingsmanager.ts index 497a9a2d4..27432ecbe 100644 --- a/src/ts/components/subtitlesettings/subtitlesettingsmanager.ts +++ b/src/ts/components/subtitlesettings/subtitlesettingsmanager.ts @@ -18,6 +18,9 @@ interface Properties { [name: string]: SubtitleSettingsProperty; } +/** + * @category Utils + */ export class SubtitleSettingsManager { private userSettings: SubtitleSettings; private localStorageKey: string; diff --git a/src/ts/components/subtitlesettings/subtitlesettingspanelpage.ts b/src/ts/components/subtitlesettings/subtitlesettingspanelpage.ts index 25792f86a..95b560a53 100644 --- a/src/ts/components/subtitlesettings/subtitlesettingspanelpage.ts +++ b/src/ts/components/subtitlesettings/subtitlesettingspanelpage.ts @@ -20,11 +20,17 @@ import {SettingsPanelItem} from '../settingspanelitem'; import { PlayerAPI } from 'bitmovin-player'; import { i18n } from '../../localization/i18n'; +/** + * @category Configs + */ export interface SubtitleSettingsPanelPageConfig extends ContainerConfig { settingsPanel: SettingsPanel; overlay: SubtitleOverlay; } +/** + * @category Components + */ export class SubtitleSettingsPanelPage extends SettingsPanelPage { private readonly overlay: SubtitleOverlay; diff --git a/src/ts/components/subtitlesettings/subtitlesettingsresetbutton.ts b/src/ts/components/subtitlesettings/subtitlesettingsresetbutton.ts index 407378c2e..0012e6b5d 100644 --- a/src/ts/components/subtitlesettings/subtitlesettingsresetbutton.ts +++ b/src/ts/components/subtitlesettings/subtitlesettingsresetbutton.ts @@ -6,6 +6,8 @@ import { i18n } from '../../localization/i18n'; /** * A button that resets all subtitle settings to their defaults. + * + * @category Buttons */ export class SubtitleSettingsResetButton extends Button { diff --git a/src/ts/components/subtitlesettings/windowcolorselectbox.ts b/src/ts/components/subtitlesettings/windowcolorselectbox.ts index 663303ee4..98bb849ee 100644 --- a/src/ts/components/subtitlesettings/windowcolorselectbox.ts +++ b/src/ts/components/subtitlesettings/windowcolorselectbox.ts @@ -5,6 +5,8 @@ import { i18n } from '../../localization/i18n'; /** * A select box providing a selection of different background colors. + * + * @category Components */ export class WindowColorSelectBox extends SubtitleSettingSelectBox { diff --git a/src/ts/components/subtitlesettings/windowopacityselectbox.ts b/src/ts/components/subtitlesettings/windowopacityselectbox.ts index fe55a2640..3e4b72570 100644 --- a/src/ts/components/subtitlesettings/windowopacityselectbox.ts +++ b/src/ts/components/subtitlesettings/windowopacityselectbox.ts @@ -5,6 +5,8 @@ import { i18n } from '../../localization/i18n'; /** * A select box providing a selection of different background opacity. + * + * @category Components */ export class WindowOpacitySelectBox extends SubtitleSettingSelectBox { diff --git a/src/ts/components/timelinemarkershandler.ts b/src/ts/components/timelinemarkershandler.ts index 20ceeb91f..9129be107 100644 --- a/src/ts/components/timelinemarkershandler.ts +++ b/src/ts/components/timelinemarkershandler.ts @@ -7,6 +7,9 @@ import { SeekBarMarker } from './seekbar'; import { PlayerUtils } from '../playerutils'; import { Timeout } from '../timeout'; +/** + * @category Configs + */ export interface MarkersConfig extends ComponentConfig { /** * Used for seekBar marker snapping range percentage diff --git a/src/ts/components/titlebar.ts b/src/ts/components/titlebar.ts index 73282abed..1f2579d38 100644 --- a/src/ts/components/titlebar.ts +++ b/src/ts/components/titlebar.ts @@ -5,6 +5,8 @@ import { PlayerAPI } from 'bitmovin-player'; /** * Configuration interface for a {@link TitleBar}. + * + * @category Configs */ export interface TitleBarConfig extends ContainerConfig { /** @@ -17,6 +19,8 @@ export interface TitleBarConfig extends ContainerConfig { /** * Displays a title bar containing a label with the title of the video. + * + * @category Components */ export class TitleBar extends Container { diff --git a/src/ts/components/togglebutton.ts b/src/ts/components/togglebutton.ts index d1c63de87..e7d4560fa 100644 --- a/src/ts/components/togglebutton.ts +++ b/src/ts/components/togglebutton.ts @@ -6,6 +6,8 @@ import { LocalizableText } from '../localization/i18n'; /** * Configuration interface for a toggle button component. + * + * @category Configs */ export interface ToggleButtonConfig extends ButtonConfig { /** @@ -41,6 +43,8 @@ export interface ToggleButtonConfig extends ButtonConfig { /** * A button that can be toggled between 'on' and 'off' states. + * + * @category Components */ export class ToggleButton extends Button { diff --git a/src/ts/components/tvnoisecanvas.ts b/src/ts/components/tvnoisecanvas.ts index f0a8148b3..664ac4d1d 100644 --- a/src/ts/components/tvnoisecanvas.ts +++ b/src/ts/components/tvnoisecanvas.ts @@ -3,6 +3,8 @@ import {DOM} from '../dom'; /** * Animated analog TV static noise. + * + * @category Components */ export class TvNoiseCanvas extends Component { @@ -28,7 +30,7 @@ export class TvNoiseCanvas extends Component { } protected toDomElement(): DOM { - return this.canvas = new DOM('canvas', { 'class': this.getCssClasses() }); + return this.canvas = new DOM('canvas', { 'class': this.getCssClasses() }, this); } start(): void { diff --git a/src/ts/components/uicontainer.ts b/src/ts/components/uicontainer.ts index baebe317a..e09ba1ecc 100644 --- a/src/ts/components/uicontainer.ts +++ b/src/ts/components/uicontainer.ts @@ -1,14 +1,17 @@ -import {ContainerConfig, Container} from './container'; -import {UIInstanceManager} from '../uimanager'; -import {DOM} from '../dom'; -import {Timeout} from '../timeout'; -import {PlayerUtils} from '../playerutils'; -import { CancelEventArgs, EventDispatcher } from '../eventdispatcher'; +import { Container, ContainerConfig } from './container'; +import { UIInstanceManager } from '../uimanager'; +import { DOM, HTMLElementWithComponent } from '../dom'; +import { Timeout } from '../timeout'; +import { PlayerUtils } from '../playerutils'; +import { CancelEventArgs, Event as UiEvent, EventDispatcher } from '../eventdispatcher'; import { PlayerAPI, PlayerResizedEvent } from 'bitmovin-player'; import { i18n } from '../localization/i18n'; +import { Button, ButtonConfig } from './button'; /** * Configuration interface for a {@link UIContainer}. + * + * @category Configs */ export interface UIContainerConfig extends ContainerConfig { /** @@ -39,6 +42,8 @@ export interface UIContainerConfig extends ContainerConfig { /** * The base container that contains all of the UI. The UIContainer is passed to the {@link UIManager} to build and * setup the UI. + * + * @category Containers */ export class UIContainer extends Container { @@ -55,6 +60,7 @@ export class UIContainer extends Container { private userInteractionEventSource: DOM; private userInteractionEvents: { name: string, handler: EventListenerOrEventListenerObject }[]; + private hidingPrevented: () => boolean; public hideUi: () => void = () => {}; public showUi: () => void = () => {}; @@ -71,6 +77,7 @@ export class UIContainer extends Container { }, this.config); this.playerStateChange = new EventDispatcher(); + this.hidingPrevented = () => false; } configure(player: PlayerAPI, uimanager: UIInstanceManager): void { @@ -101,7 +108,7 @@ export class UIContainer extends Container { let isFirstTouch = true; let playerState: PlayerUtils.PlayerState; - const hidingPrevented = (): boolean => { + this.hidingPrevented = (): boolean => { return config.hidePlayerStateExceptions && config.hidePlayerStateExceptions.indexOf(playerState) > -1; }; @@ -112,7 +119,7 @@ export class UIContainer extends Container { isUiShown = true; } // Don't trigger timeout while seeking (it will be triggered once the seek is finished) or casting - if (!isSeeking && !player.isCasting() && !hidingPrevented()) { + if (!isSeeking && !player.isCasting() && !this.hidingPrevented()) { this.uiHideTimeout.start(); } }; @@ -142,6 +149,27 @@ export class UIContainer extends Container { // On touch displays, the first touch reveals the UI name: 'touchend', handler: (e) => { + const shouldPreventDefault = ((e: Event): Boolean => { + const findButtonComponent = ((element: HTMLElementWithComponent): Button | null => { + if ( + !element + || element === this.userInteractionEventSource.get(0) + || element.component instanceof UIContainer + ) { + return null; + } + + if (element.component && element.component instanceof Button) { + return element.component; + } else { + return findButtonComponent(element.parentElement); + } + }); + + const buttonComponent = findButtonComponent(e.target as HTMLElementWithComponent); + return !(buttonComponent && buttonComponent.getConfig().acceptsTouchWithUiHidden); + }); + if (!isUiShown) { // Only if the UI is hidden, we prevent other actions (except for the first touch) and reveal the UI // instead. The first touch is not prevented to let other listeners receive the event and trigger an @@ -150,7 +178,14 @@ export class UIContainer extends Container { if (isFirstTouch && !player.isPlaying()) { isFirstTouch = false; } else { - e.preventDefault(); + // On touch input devices, the first touch is expected to display the UI controls and not be propagated to + // other components. + // When buttons are always visible this causes UX problems, as the first touch is not recognized. + // This is the case for the {@link AdSkipButton} and {@link AdClickOverlay}. + // To prevent UX issues where the buttons need to be touched twice, we do not prevent the first touch event. + if (shouldPreventDefault(e)) { + e.preventDefault(); + } } this.showUi(); } @@ -183,7 +218,7 @@ export class UIContainer extends Container { handler: () => { // When a seek is going on, the seek scrub pointer may exit the UI area while still seeking, and we do not // hide the UI in such cases - if (!isSeeking && !hidingPrevented()) { + if (!isSeeking && !this.hidingPrevented()) { if (this.config.hideImmediatelyOnMouseLeave) { this.hideUi(); } else { @@ -201,16 +236,17 @@ export class UIContainer extends Container { }); uimanager.onSeeked.subscribe(() => { isSeeking = false; - if (!hidingPrevented()) { + if (!this.hidingPrevented()) { this.uiHideTimeout.start(); // Re-enable UI hide timeout after a seek } }); + uimanager.onComponentViewModeChanged.subscribe((_, { mode }) => this.trackComponentViewMode(mode)); player.on(player.exports.PlayerEvent.CastStarted, () => { this.showUi(); // Show UI when a Cast session has started (UI will then stay permanently on during the session) }); this.playerStateChange.subscribe((_, state) => { playerState = state; - if (hidingPrevented()) { + if (this.hidingPrevented()) { // Entering a player state that prevents hiding and forces the controls to be shown this.uiHideTimeout.clear(); this.showUi(); @@ -359,6 +395,18 @@ export class UIContainer extends Container { } } + onPlayerStateChange(): UiEvent { + return this.playerStateChange.getEvent(); + } + + protected suspendHideTimeout() { + this.uiHideTimeout.suspend(); + } + + protected resumeHideTimeout() { + this.uiHideTimeout.resume(!this.hidingPrevented()); + } + protected toDomElement(): DOM { let container = super.toDomElement(); diff --git a/src/ts/components/videoqualityselectbox.ts b/src/ts/components/videoqualityselectbox.ts index 4a9391439..43c17daf6 100644 --- a/src/ts/components/videoqualityselectbox.ts +++ b/src/ts/components/videoqualityselectbox.ts @@ -6,6 +6,8 @@ import { i18n } from '../localization/i18n'; /** * A select box providing a selection between 'auto' and the available video qualities. + * + * @category Components */ export class VideoQualitySelectBox extends SelectBox { diff --git a/src/ts/components/volumecontrolbutton.ts b/src/ts/components/volumecontrolbutton.ts index 3b6cd81d4..3cce5d0bc 100644 --- a/src/ts/components/volumecontrolbutton.ts +++ b/src/ts/components/volumecontrolbutton.ts @@ -7,6 +7,8 @@ import { PlayerAPI } from 'bitmovin-player'; /** * Configuration interface for a {@link VolumeControlButton}. + * + * @category Configs */ export interface VolumeControlButtonConfig extends ContainerConfig { /** @@ -27,6 +29,8 @@ export interface VolumeControlButtonConfig extends ContainerConfig { /** * A composite volume control that consists of and internally manages a volume control button that can be used * for muting, and a (depending on the CSS style, e.g. slide-out) volume control bar. + * + * @category Buttons */ export class VolumeControlButton extends Container { diff --git a/src/ts/components/volumeslider.ts b/src/ts/components/volumeslider.ts index 8b64c0128..02f126753 100644 --- a/src/ts/components/volumeslider.ts +++ b/src/ts/components/volumeslider.ts @@ -6,6 +6,8 @@ import { i18n } from '../localization/i18n'; /** * Configuration interface for the {@link VolumeSlider} component. + * + * @category Configs */ export interface VolumeSliderConfig extends SeekBarConfig { /** @@ -18,6 +20,8 @@ export interface VolumeSliderConfig extends SeekBarConfig { /** * A simple volume slider component to adjust the player's volume setting. + * + * @category Components */ export class VolumeSlider extends SeekBar { private volumeTransition: VolumeTransition; diff --git a/src/ts/components/volumetogglebutton.ts b/src/ts/components/volumetogglebutton.ts index 63838ce07..583cb1db8 100644 --- a/src/ts/components/volumetogglebutton.ts +++ b/src/ts/components/volumetogglebutton.ts @@ -5,6 +5,8 @@ import { i18n } from '../localization/i18n'; /** * A button that toggles audio muting. + * + * @category Buttons */ export class VolumeToggleButton extends ToggleButton { diff --git a/src/ts/components/vrtogglebutton.ts b/src/ts/components/vrtogglebutton.ts index 62b3c91e0..61856378e 100644 --- a/src/ts/components/vrtogglebutton.ts +++ b/src/ts/components/vrtogglebutton.ts @@ -5,6 +5,8 @@ import { i18n } from '../localization/i18n'; /** * A button that toggles the video view between normal/mono and VR/stereo. + * + * @category Buttons */ export class VRToggleButton extends ToggleButton { diff --git a/src/ts/components/watermark.ts b/src/ts/components/watermark.ts index faba1ba4e..75da7f92d 100644 --- a/src/ts/components/watermark.ts +++ b/src/ts/components/watermark.ts @@ -3,6 +3,8 @@ import { i18n } from '../localization/i18n'; /** * Configuration interface for a {@link ClickOverlay}. + * + * @category Configs */ export interface WatermarkConfig extends ClickOverlayConfig { // nothing yet @@ -10,6 +12,8 @@ export interface WatermarkConfig extends ClickOverlayConfig { /** * A watermark overlay with a clickable logo. + * + * @category Components */ export class Watermark extends ClickOverlay { diff --git a/src/ts/demofactory.ts b/src/ts/demofactory.ts index cf608737a..e9f5eb144 100644 --- a/src/ts/demofactory.ts +++ b/src/ts/demofactory.ts @@ -34,6 +34,7 @@ import {SettingsPanelPage} from './components/settingspanelpage'; import { UIFactory } from './uifactory'; import { UIConfig } from './uiconfig'; import { PlayerAPI } from 'bitmovin-player'; +import { QuickSeekButton } from './main'; export namespace DemoFactory { @@ -118,6 +119,8 @@ export namespace DemoFactory { new Container({ components: [ new PlaybackToggleButton(), + new QuickSeekButton({ seekSeconds: -10 }), + new QuickSeekButton({ seekSeconds: 10 }), new VolumeToggleButton(), new VolumeSlider(), new Spacer(), diff --git a/src/ts/dom.ts b/src/ts/dom.ts index 9142d5307..a3b32fd8a 100644 --- a/src/ts/dom.ts +++ b/src/ts/dom.ts @@ -1,3 +1,5 @@ +import { Component, ComponentConfig } from './components/component'; + export interface Offset { left: number; top: number; @@ -12,6 +14,13 @@ export interface CssProperties { [propertyName: string]: string; } +/** + * Extends the {@link HTMLElement} interface with a component attribute to store the associated component. + */ +export interface HTMLElementWithComponent extends HTMLElement { + component?: Component; +} + /** * Simple DOM manipulation and DOM element event handling modeled after jQuery (as replacement for jQuery). * @@ -31,14 +40,15 @@ export class DOM { * The list of elements that the instance wraps. Take care that not all methods can operate on the whole list, * getters usually just work on the first element. */ - private elements: HTMLElement[]; + private elements: HTMLElementWithComponent[]; /** * Creates a DOM element. * @param tagName the tag name of the DOM element * @param attributes a list of attributes of the element + * @param component the {@link Component} the DOM element is associated with */ - constructor(tagName: string, attributes: {[name: string]: string}); + constructor(tagName: string, attributes: {[name: string]: string}, component?: Component); /** * Selects all elements from the DOM that match the specified selector. * @param selector the selector to match DOM elements with @@ -59,17 +69,21 @@ export class DOM { * @param document the document to wrap */ constructor(document: Document); - constructor(something: string | HTMLElement | HTMLElement[] | Document, attributes?: {[name: string]: string}) { + constructor( + something: string | HTMLElement | HTMLElement[] | Document, + attributes?: {[name: string]: string}, + component?: Component, + ) { this.document = document; // Set the global document to the local document field if (something instanceof Array) { if (something.length > 0 && something[0] instanceof HTMLElement) { - let elements = something; + let elements = something as HTMLElementWithComponent[]; this.elements = elements; } } else if (something instanceof HTMLElement) { - let element = something; + let element = something as HTMLElementWithComponent; this.elements = [element]; } else if (something instanceof Document) { @@ -80,7 +94,7 @@ export class DOM { } else if (attributes) { let tagName = something; - let element = document.createElement(tagName); + let element = document.createElement(tagName) as HTMLElementWithComponent; for (let attributeName in attributes) { let attributeValue = attributes[attributeName]; @@ -89,11 +103,15 @@ export class DOM { } } + if (component) { + element.component = component; + } + this.elements = [element]; } else { let selector = something; - this.elements = this.findChildElements(selector); + this.elements = this.findChildElements(selector) as HTMLElementWithComponent[]; } } @@ -109,14 +127,14 @@ export class DOM { * Gets the HTML elements that this DOM instance currently holds. * @returns {HTMLElement[]} the raw HTML elements */ - get(): HTMLElement[]; + get(): HTMLElementWithComponent[]; /** * Gets an HTML element from the list elements that this DOM instance currently holds. * @param index The zero-based index into the element list. Can be negative to return an element from the end, * e.g. -1 returns the last element. */ - get(index: number): HTMLElement; - get(index?: number): HTMLElement | HTMLElement[] { + get(index: number): HTMLElementWithComponent; + get(index?: number): HTMLElementWithComponent | HTMLElementWithComponent[] { if (index === undefined) { return this.elements; } else if (!this.elements || index >= this.elements.length || index < -this.elements.length) { @@ -170,7 +188,7 @@ export class DOM { * @returns {DOM} a new DOM instance representing all matched children */ find(selector: string): DOM { - let allChildElements = this.findChildElements(selector); + let allChildElements = this.findChildElements(selector) as HTMLElementWithComponent[]; return new DOM(allChildElements); } @@ -413,18 +431,19 @@ export class DOM { * Attaches an event handler to one or more events on all elements. * @param eventName the event name (or multiple names separated by space) to listen to * @param eventHandler the event handler to call when the event fires + * @param options the options for this event handler * @returns {DOM} */ - on(eventName: string, eventHandler: EventListenerOrEventListenerObject): DOM { + on(eventName: string, eventHandler: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): DOM { let events = eventName.split(' '); events.forEach((event) => { if (this.elements == null) { - this.document.addEventListener(event, eventHandler); + this.document.addEventListener(event, eventHandler, options); } else { this.forEach((element) => { - element.addEventListener(event, eventHandler); + element.addEventListener(event, eventHandler, options); }); } }); @@ -436,18 +455,19 @@ export class DOM { * Removes an event handler from one or more events on all elements. * @param eventName the event name (or multiple names separated by space) to remove the handler from * @param eventHandler the event handler to remove + * @param options the options for this event handler * @returns {DOM} */ - off(eventName: string, eventHandler: EventListenerOrEventListenerObject): DOM { + off(eventName: string, eventHandler: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): DOM { let events = eventName.split(' '); events.forEach((event) => { if (this.elements == null) { - this.document.removeEventListener(event, eventHandler); + this.document.removeEventListener(event, eventHandler, options); } else { this.forEach((element) => { - element.removeEventListener(event, eventHandler); + element.removeEventListener(event, eventHandler, options); }); } }); diff --git a/src/ts/errorutils.ts b/src/ts/errorutils.ts index a14494aa7..76b380632 100644 --- a/src/ts/errorutils.ts +++ b/src/ts/errorutils.ts @@ -2,6 +2,9 @@ import {ErrorMessageMap, ErrorMessageTranslator} from './components/errormessage import { ErrorEvent } from 'bitmovin-player'; import { MobileV3PlayerErrorEvent, MobileV3SourceErrorEvent } from './mobilev3playerapi'; +/** + * @category Utils + */ export namespace ErrorUtils { export const defaultErrorMessages: ErrorMessageMap = { diff --git a/src/ts/localization/i18n.ts b/src/ts/localization/i18n.ts index aef22f73c..549f21632 100644 --- a/src/ts/localization/i18n.ts +++ b/src/ts/localization/i18n.ts @@ -1,12 +1,15 @@ import vocabularyDe from './languages/de.json'; import vocabularyEn from './languages/en.json'; import vocabularyEs from './languages/es.json'; +import vocabularyNl from './languages/nl.json'; + import { LocalizationConfig } from '../uimanager.js'; export const defaultVocabularies: Vocabularies = { 'en': vocabularyEn, 'de': vocabularyDe, 'es': vocabularyEs, + 'nl': vocabularyNl, }; const defaultLocalizationConfig: LocalizationConfig = { @@ -14,10 +17,19 @@ const defaultLocalizationConfig: LocalizationConfig = { vocabularies: defaultVocabularies, }; -type Localizer = () => string; +/** + * @category Localization + */ +export type Localizer = () => string; +/** + * @category Localization + */ export type LocalizableText = string | Localizer; -interface Vocabulary { +/** + * @category Localization + */ +export interface Vocabulary { 'settings.video.quality': string; 'settings.audio.quality': string; 'settings.audio.track': string; @@ -87,15 +99,26 @@ interface Vocabulary { 'seekBar.value': string; 'seekBar.timeshift': string; 'seekBar.durationText': string; + 'ecoMode': string; + 'ecoMode.title': string; } +/** + * @category Localization + */ export type CustomVocabulary = V & Partial; +/** + * @category Localization + */ export interface Vocabularies { [key: string]: CustomVocabulary>; } -class I18n { +/** + * @category Localization + */ +export class I18n { private language: string; private vocabulary: CustomVocabulary>; @@ -191,4 +214,7 @@ class I18n { } } +/** + * @category Localization + */ export const i18n = new I18n(defaultLocalizationConfig); diff --git a/src/ts/localization/languages/de.json b/src/ts/localization/languages/de.json index fd48e2a3d..f8fe68b80 100644 --- a/src/ts/localization/languages/de.json +++ b/src/ts/localization/languages/de.json @@ -51,5 +51,9 @@ "seekBar": "Video-Timeline", "seekBar.value": "Wert", "seekBar.timeshift": "Timeshift", - "seekBar.durationText": "aus" + "seekBar.durationText": "aus", + "quickseek.forward": "Vor", + "quickseek.rewind": "Zurück", + "ecoMode": "ecoMode", + "ecoMode.title":"Eco Mode" } diff --git a/src/ts/localization/languages/en.json b/src/ts/localization/languages/en.json index a32266bb9..ffc1167dc 100644 --- a/src/ts/localization/languages/en.json +++ b/src/ts/localization/languages/en.json @@ -52,6 +52,8 @@ "vr" : "VR", "off": "off", "auto": "auto", + "ecoMode": "ecoMode", + "ecoMode.title":"Eco Mode", "back" : "Back", "reset": "Reset", "replay": "Replay", @@ -68,5 +70,7 @@ "seekBar": "Video timeline", "seekBar.value": "Value", "seekBar.timeshift": "Timeshift", - "seekBar.durationText": "out of" + "seekBar.durationText": "out of", + "quickseek.forward": "Fast Forward", + "quickseek.rewind": "Rewind" } diff --git a/src/ts/localization/languages/es.json b/src/ts/localization/languages/es.json index 5d09414f9..22224d616 100644 --- a/src/ts/localization/languages/es.json +++ b/src/ts/localization/languages/es.json @@ -52,6 +52,8 @@ "vr" : "VR", "off": "off", "auto": "auto", + "ecoMode": "ecoMode", + "ecoMode.title": "Eco Mode", "back" : "Atrás", "reset": "Reiniciar", "replay": "Rebobinar", @@ -68,5 +70,7 @@ "seekBar": "Línea de Tiempo", "seekBar.value": "posición", "seekBar.timeshift": "cambio de posición", - "seekBar.durationText": "de" + "seekBar.durationText": "de", + "quickseek.forward": "Adelantar", + "quickseek.rewind": "Rebobinar" } \ No newline at end of file diff --git a/src/ts/localization/languages/nl.json b/src/ts/localization/languages/nl.json new file mode 100644 index 000000000..9e2a1229c --- /dev/null +++ b/src/ts/localization/languages/nl.json @@ -0,0 +1,76 @@ +{ + "settings.video.quality": "Videokwaliteit", + "settings.audio.quality": "Audiokwaliteit", + "settings.audio.track": "Audiospoor", + "settings.audio.mute": "Dempen", + "settings.audio.volume": "Volume", + "settings.subtitles.window.color": "Vensterkleur", + "settings.subtitles.window.opacity": "Venster doorzichtigheid", + "settings.subtitles": "Ondertiteling", + "settings.subtitles.font.color": "Lettertype kleur", + "settings.subtitles.font.opacity": "Lettertype doorzichtigheid", + "settings.subtitles.background.color": "Achtergrondkleur", + "settings.subtitles.background.opacity": "Achtergrond doorzichtigheid", + "colors.white": "wit", + "colors.black": "zwart", + "colors.red": "rood", + "colors.green": "groen", + "colors.blue": "blauw", + "colors.cyan": "cyaan", + "colors.yellow": "geel", + "colors.magenta": "magenta", + "percent": "{value}%", + "settings.subtitles.font.size": "Lettertype grootte", + "settings.subtitles.characterEdge": "Lettertype rand", + "settings.subtitles.characterEdge.raised": "verhoogd", + "settings.subtitles.characterEdge.depressed": "verlaagd", + "settings.subtitles.characterEdge.uniform": "uniform", + "settings.subtitles.characterEdge.dropshadowed": "schaduw", + "settings.subtitles.font.family": "Standaard lettertype", + "settings.subtitles.font.family.monospacedserif": "monospace serif", + "settings.subtitles.font.family.proportionalserif": "proportioneel serif", + "settings.subtitles.font.family.monospacedsansserif": "monospace sans-serif", + "settings.subtitles.font.family.proportionalsansserif": "proportioneel sans-serif", + "settings.subtitles.font.family.casual": "casual", + "settings.subtitles.font.family.cursive": "cursief", + "settings.subtitles.font.family.smallcapital": "kleine hoofdletters", + "settings.time.hours": "Uren", + "settings.time.minutes": "Minuten", + "settings.time.seconds": "Seconden", + "ads.remainingTime": "Deze advertentie eindigt in {remainingTime} seconden.", + "settings": "Instellingen", + "fullscreen": "Volledig scherm", + "speed": "Snelheid", + "playPause": "Afspelen/Pauzeren", + "play": "Afspelen", + "pause": "Pauzeren", + "open": "Openen", + "close": "Sluiten", + "pictureInPicture": "Picture-in-Picture", + "appleAirplay": "Apple AirPlay", + "googleCast": "Google Cast", + "vr": "VR", + "off": "uit", + "auto": "automatisch", + "ecoMode": "Eco-modus", + "ecoMode.title": "Eco-modus", + "back": "Terug", + "reset": "Reset", + "replay": "Opnieuw afspelen", + "normal": "normaal", + "default": "standaard", + "live": "Live", + "subtitle.example": "voorbeeld ondertiteling", + "subtitle.select": "Selecteer ondertiteling", + "playingOn": "Speelt af op {castDeviceName}", + "connectingTo": "Verbinden met {castDeviceName}...", + "watermarkLink": "Link naar homepage", + "controlBar": "Videospeler bediening", + "player": "Videospeler", + "seekBar": "Video tijdlijn", + "seekBar.value": "Waarde", + "seekBar.timeshift": "Tijdverschuiving", + "seekBar.durationText": "van", + "quickseek.forward": "Vooruitspoelen", + "quickseek.rewind": "Terugspoelen" +} \ No newline at end of file diff --git a/src/ts/main.ts b/src/ts/main.ts index dd18c3127..82a7e80cb 100644 --- a/src/ts/main.ts +++ b/src/ts/main.ts @@ -1,6 +1,7 @@ export const version: string = '{{VERSION}}'; // Management -export { UIManager, UIInstanceManager, UIVariant } from './uimanager'; +export * from './uimanager'; +export * from './uiconfig'; // Factories export { UIFactory } from './uifactory'; export { DemoFactory } from './demofactory'; @@ -13,57 +14,57 @@ export { BrowserUtils } from './browserutils'; export { StorageUtils } from './storageutils'; export { ErrorUtils } from './errorutils'; // Localization -export { i18n } from './localization/i18n'; +export { i18n, I18n, Vocabulary, Vocabularies, CustomVocabulary, LocalizableText, Localizer } from './localization/i18n'; // Spatial Navigation export { SpatialNavigation } from './spatialnavigation/spatialnavigation'; export { NavigationGroup } from './spatialnavigation/navigationgroup'; export { RootNavigationGroup } from './spatialnavigation/rootnavigationgroup'; export { ListNavigationGroup, ListOrientation } from './spatialnavigation/ListNavigationGroup'; // Components -export { Button } from './components/button'; -export { ControlBar } from './components/controlbar'; +export { Button, ButtonConfig } from './components/button'; +export { ControlBar, ControlBarConfig } from './components/controlbar'; export { FullscreenToggleButton } from './components/fullscreentogglebutton'; export { HugePlaybackToggleButton } from './components/hugeplaybacktogglebutton'; -export { PlaybackTimeLabel, PlaybackTimeLabelMode } from './components/playbacktimelabel'; -export { PlaybackToggleButton } from './components/playbacktogglebutton'; -export { SeekBar } from './components/seekbar'; +export { PlaybackTimeLabel, PlaybackTimeLabelConfig, PlaybackTimeLabelMode } from './components/playbacktimelabel'; +export { PlaybackToggleButton, PlaybackToggleButtonConfig } from './components/playbacktogglebutton'; +export { SeekBar, SeekBarConfig, SeekPreviewEventArgs, SeekBarMarker } from './components/seekbar'; export { SelectBox } from './components/selectbox'; export { ItemSelectionList } from './components/itemselectionlist'; -export { SettingsPanel } from './components/settingspanel'; -export { SettingsToggleButton } from './components/settingstogglebutton'; -export { ToggleButton } from './components/togglebutton'; +export { SettingsPanel, SettingsPanelConfig } from './components/settingspanel'; +export { SettingsToggleButton, SettingsToggleButtonConfig } from './components/settingstogglebutton'; +export { ToggleButton, ToggleButtonConfig } from './components/togglebutton'; export { VideoQualitySelectBox } from './components/videoqualityselectbox'; export { VolumeToggleButton } from './components/volumetogglebutton'; export { VRToggleButton } from './components/vrtogglebutton'; -export { Watermark } from './components/watermark'; -export { UIContainer } from './components/uicontainer'; -export { Container } from './components/container'; -export { Label } from './components/label'; +export { Watermark, WatermarkConfig } from './components/watermark'; +export { UIContainer, UIContainerConfig } from './components/uicontainer'; +export { Container, ContainerConfig } from './components/container'; +export { Label, LabelConfig } from './components/label'; export { AudioQualitySelectBox } from './components/audioqualityselectbox'; export { AudioTrackSelectBox } from './components/audiotrackselectbox'; export { CastStatusOverlay } from './components/caststatusoverlay'; export { CastToggleButton } from './components/casttogglebutton'; -export { Component } from './components/component'; -export { ErrorMessageOverlay } from './components/errormessageoverlay'; +export { Component, ComponentConfig, ComponentHoverChangedEventArgs } from './components/component'; +export { ErrorMessageOverlay, ErrorMessageOverlayConfig, ErrorMessageTranslator, ErrorMessageMap } from './components/errormessageoverlay'; export { RecommendationOverlay } from './components/recommendationoverlay'; -export { SeekBarLabel } from './components/seekbarlabel'; +export { SeekBarLabel, SeekBarLabelConfig } from './components/seekbarlabel'; export { SubtitleOverlay } from './components/subtitleoverlay'; export { SubtitleSelectBox } from './components/subtitleselectbox'; -export { TitleBar } from './components/titlebar'; -export { VolumeControlButton } from './components/volumecontrolbutton'; -export { ClickOverlay } from './components/clickoverlay'; -export { AdSkipButton } from './components/adskipbutton'; +export { TitleBar, TitleBarConfig } from './components/titlebar'; +export { VolumeControlButton, VolumeControlButtonConfig } from './components/volumecontrolbutton'; +export { ClickOverlay, ClickOverlayConfig } from './components/clickoverlay'; +export { AdSkipButton, AdSkipButtonConfig } from './components/adskipbutton'; export { AdMessageLabel } from './components/admessagelabel'; export { AdClickOverlay } from './components/adclickoverlay'; export { PlaybackSpeedSelectBox } from './components/playbackspeedselectbox'; export { HugeReplayButton } from './components/hugereplaybutton'; -export { BufferingOverlay } from './components/bufferingoverlay'; +export { BufferingOverlay, BufferingOverlayConfig } from './components/bufferingoverlay'; export { CastUIContainer } from './components/castuicontainer'; -export { PlaybackToggleOverlay } from './components/playbacktoggleoverlay'; -export { CloseButton } from './components/closebutton'; -export { MetadataLabel, MetadataLabelContent } from './components/metadatalabel'; +export { PlaybackToggleOverlay, PlaybackToggleOverlayConfig } from './components/playbacktoggleoverlay'; +export { CloseButton, CloseButtonConfig } from './components/closebutton'; +export { MetadataLabel, MetadataLabelContent, MetadataLabelConfig } from './components/metadatalabel'; export { AirPlayToggleButton } from './components/airplaytogglebutton'; -export { VolumeSlider } from './components/volumeslider'; +export { VolumeSlider, VolumeSliderConfig } from './components/volumeslider'; export { PictureInPictureToggleButton } from './components/pictureinpicturetogglebutton'; export { Spacer } from './components/spacer'; export { BackgroundColorSelectBox } from './components/subtitlesettings/backgroundcolorselectbox'; @@ -84,9 +85,11 @@ export { AudioTrackListBox } from './components/audiotracklistbox'; export { SettingsPanelPage } from './components/settingspanelpage'; export { SettingsPanelPageBackButton } from './components/settingspanelpagebackbutton'; export { SettingsPanelPageOpenButton } from './components/settingspanelpageopenbutton'; -export { SubtitleSettingsPanelPage } from './components/subtitlesettings/subtitlesettingspanelpage'; +export { SubtitleSettingsPanelPage, SubtitleSettingsPanelPageConfig } from './components/subtitlesettings/subtitlesettingspanelpage'; export { SettingsPanelItem } from './components/settingspanelitem'; export { ReplayButton } from './components/replaybutton'; +export { QuickSeekButton, QuickSeekButtonConfig } from './components/quickseekbutton'; +export { ListSelector, ListSelectorConfig, ListItem, ListItemFilter, ListItemLabelTranslator } from './components/listselector'; // Object.assign polyfill for ES5/IE9 // https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Object/assign diff --git a/src/ts/playerutils.ts b/src/ts/playerutils.ts index a4ff2187f..36c436301 100644 --- a/src/ts/playerutils.ts +++ b/src/ts/playerutils.ts @@ -3,6 +3,9 @@ import {BrowserUtils} from './browserutils'; import { UIInstanceManager } from './uimanager'; import { PlayerAPI, TimeRange } from 'bitmovin-player'; +/** + * @category Utils + */ export namespace PlayerUtils { export enum PlayerState { @@ -195,4 +198,10 @@ export namespace PlayerUtils { return this.liveChangedEvent.getEvent(); } } + + export function clampValueToRange(value: number, boundary1: number, boundary2: number): number { + const lowerBoundary = Math.min(boundary1, boundary2); + const upperBoundary = Math.max(boundary1, boundary2); + return Math.min(Math.max(value, lowerBoundary), upperBoundary); + } } diff --git a/src/ts/spatialnavigation/ListNavigationGroup.ts b/src/ts/spatialnavigation/ListNavigationGroup.ts index 7b1031bdd..6dcfd3cdd 100644 --- a/src/ts/spatialnavigation/ListNavigationGroup.ts +++ b/src/ts/spatialnavigation/ListNavigationGroup.ts @@ -8,6 +8,9 @@ export enum ListOrientation { Vertical = 'vertical', } +/** + * @category Components + */ export class ListNavigationGroup extends NavigationGroup { private readonly listNavigationDirections: Direction[]; diff --git a/src/ts/spatialnavigation/navigationgroup.ts b/src/ts/spatialnavigation/navigationgroup.ts index 149627e68..fd5c5c45c 100644 --- a/src/ts/spatialnavigation/navigationgroup.ts +++ b/src/ts/spatialnavigation/navigationgroup.ts @@ -13,6 +13,8 @@ import { Action, ActionCallback, Callback, Direction, NavigationCallback } from * Responsible for finding elements in direction on navigation and for tracking active element inside the group. * Triggers blur and focus on element when active element is changed, as well as click on element on `Action.SELECT`. * Will call `hideUi()` on passed in container if `Action.BACK` is called. + * + * @category Components */ export class NavigationGroup { private activeElement?: HTMLElement; diff --git a/src/ts/spatialnavigation/rootnavigationgroup.ts b/src/ts/spatialnavigation/rootnavigationgroup.ts index 959749cb9..80a394307 100644 --- a/src/ts/spatialnavigation/rootnavigationgroup.ts +++ b/src/ts/spatialnavigation/rootnavigationgroup.ts @@ -5,6 +5,8 @@ import { Action, Direction } from './types'; /** * Extends NavigationGroup and provides additional logic for hiding and showing the UI on the root container. + * + * @category Components */ export class RootNavigationGroup extends NavigationGroup { constructor(public readonly container: UIContainer, ...elements: Component[]) { diff --git a/src/ts/storageutils.ts b/src/ts/storageutils.ts index bd5cedb5f..34e06b2c0 100644 --- a/src/ts/storageutils.ts +++ b/src/ts/storageutils.ts @@ -1,5 +1,8 @@ import { UIConfig } from './uiconfig'; +/** + * @category Utils + */ export namespace StorageUtils { let disableStorageApi: boolean; diff --git a/src/ts/stringutils.ts b/src/ts/stringutils.ts index a1a6782d6..f54c1326d 100644 --- a/src/ts/stringutils.ts +++ b/src/ts/stringutils.ts @@ -1,6 +1,9 @@ -import { PlayerAPI } from 'bitmovin-player'; +import { Ad, LinearAd, PlayerAPI } from 'bitmovin-player'; import { i18n } from './localization/i18n'; +/** + * @category Utils + */ export namespace StringUtils { export let FORMAT_HHMMSS: string = 'hh:mm:ss'; @@ -70,8 +73,8 @@ export namespace StringUtils { /** * Fills out placeholders in an ad message. * - * Has the placeholders '{remainingTime[formatString]}', '{playedTime[formatString]}' and - * '{adDuration[formatString]}', which are replaced by the remaining time until the ad can be skipped, the current + * Has the placeholders '{remainingTime[formatString]}', '{playedTime[formatString]}', + * '{adDuration[formatString]}' and {adBreakRemainingTime[formatString]}, which are replaced by the remaining time until the ad can be skipped, the current * time or the ad duration. The format string is optional. If not specified, the placeholder is replaced by the time * in seconds. If specified, it must be of the following format: * - %d - Inserts the time as an integer. @@ -88,6 +91,8 @@ export namespace StringUtils { * An input value of 100 would be displayed as: 'Ad: 01:40 secs' * - { text: 'Ad: {remainingTime%f} secs' } * An input value of 100 would be displayed as: 'Ad: 100.0 secs' + * - { text: 'Adbreak: {adBreakRemainingTime%f} secs' } + * Adbreak with 2 ads each 50 seconds would be displayed as: 'Ad: 100.0 secs' * * @param adMessage an ad message with optional placeholders to fill * @param skipOffset if specified, {remainingTime} will be filled with the remaining time until the ad can be skipped @@ -96,7 +101,7 @@ export namespace StringUtils { */ export function replaceAdMessagePlaceholders(adMessage: string, skipOffset: number, player: PlayerAPI) { let adMessagePlaceholderRegex = new RegExp( - '\\{(remainingTime|playedTime|adDuration)(}|%((0[1-9]\\d*(\\.\\d+(d|f)|d|f)|\\.\\d+f|d|f)|hh:mm:ss|mm:ss)})', + '\\{(remainingTime|playedTime|adDuration|adBreakRemainingTime)(}|%((0[1-9]\\d*(\\.\\d+(d|f)|d|f)|\\.\\d+f|d|f)|hh:mm:ss|mm:ss)})', 'g', ); @@ -112,7 +117,22 @@ export namespace StringUtils { time = player.getCurrentTime(); } else if (formatString.indexOf('adDuration') > -1) { time = player.getDuration(); + } else if (formatString.indexOf('adBreakRemainingTime') > -1) { // To display the remaining time in the ad break as opposed to in the ad + time = 0; + + // compute list of ads and calculate duration of remaining ads based on index of active ad + if (player.ads.isLinearAdActive()) { + const isActiveAd = (ad: Ad) => player.ads.getActiveAd().id === ad.id; + const indexOfActiveAd = player.ads.getActiveAdBreak().ads.findIndex(isActiveAd); + const duration = player.ads.getActiveAdBreak().ads + .slice(indexOfActiveAd) + .reduce((total, ad) => total + (ad.isLinear ? (ad as LinearAd).duration : 0), 0); + + // And remaning ads duration minus time played + time = duration - player.getCurrentTime(); + } } + return formatNumber(Math.round(time), formatString); }); } diff --git a/src/ts/subtitleutils.ts b/src/ts/subtitleutils.ts index 79d60650a..90555b088 100644 --- a/src/ts/subtitleutils.ts +++ b/src/ts/subtitleutils.ts @@ -7,6 +7,8 @@ import { i18n } from './localization/i18n'; * Helper class to handle all subtitle related events * * This class listens to player events as well as the `ListSelector` event if selection changed + * + * @category Utils */ export class SubtitleSwitchHandler { diff --git a/src/ts/timeout.ts b/src/ts/timeout.ts index 1dc635dfb..5f75d7d8a 100644 --- a/src/ts/timeout.ts +++ b/src/ts/timeout.ts @@ -14,6 +14,7 @@ export class Timeout { // this value except providing it to clearTimeout. private timeoutOrIntervalId: any; private active: boolean; + private suspended: boolean; /** * Creates a new timeout callback handler. @@ -27,10 +28,12 @@ export class Timeout { this.repeat = repeat; this.timeoutOrIntervalId = 0; this.active = false; + this.suspended = false; } /** - * Starts the timeout and calls the callback when the timeout delay has passed. + * Starts the timeout and calls the callback when the timeout delay has passed. Has no effect when the timeout is + * suspended. * @returns {Timeout} the current timeout (so the start call can be chained to the constructor) */ start(): this { @@ -46,11 +49,41 @@ export class Timeout { } /** - * Resets the passed timeout delay to zero. Can be used to defer the calling of the callback. + * Suspends the timeout. The callback will not be called and calls to `start` and `reset` will be ignored until the + * timeout is resumed. + */ + suspend() { + this.suspended = true; + this.clearInternal(); + + return this; + } + + /** + * Resumes the timeout. + * @param reset whether to reset the timeout after resuming + */ + resume(reset: boolean) { + this.suspended = false; + + if (reset) { + this.reset(); + } + + return this; + } + + /** + * Resets the passed timeout delay to zero. Can be used to defer the calling of the callback. Has no effect if the + * timeout is suspended. */ reset(): void { this.clearInternal(); + if (this.suspended) { + return; + } + if (this.repeat) { this.timeoutOrIntervalId = setInterval(this.callback, this.delay); } else { diff --git a/src/ts/uiconfig.ts b/src/ts/uiconfig.ts index 4ca74a33d..1dc9c9c88 100644 --- a/src/ts/uiconfig.ts +++ b/src/ts/uiconfig.ts @@ -1,5 +1,8 @@ import { ErrorMessageMap, ErrorMessageTranslator } from './components/errormessageoverlay'; +/** + * @category Configs + */ export interface UIRecommendationConfig { title: string; url: string; @@ -43,6 +46,9 @@ export interface TimelineMarker { cssClasses?: string[]; } +/** + * @category Configs + */ export interface UIConfig { /** * Specifies the container in the DOM into which the UI will be added. Can be a CSS selector string or a @@ -70,8 +76,8 @@ export interface UIConfig { playbackSpeedSelectionEnabled?: boolean; /** * Specifies if the player controls including `SettingsPanel` should auto hide when still hovered. This - * configuration does not apply to mobile platforms. On mobile platforms the `SettingsPanel` is by default - * configured to not auto-hide and the behaviour cannot be changed using this configuration. + * configuration does not apply to devices using a touch screen. On touch screen devices the `SettingsPanel` + * is by default configured to not auto-hide and the behaviour cannot be changed using this configuration. * Default: false */ disableAutoHideWhenHovered?: boolean; @@ -121,4 +127,8 @@ export interface UIConfig { * If set to true, prevents the UI from using `localStorage`. */ disableStorageApi?: boolean; + /** + * Specifies if the `EcoModeToggleButton` should be displayed within the `SettingsPanel` + */ + ecoMode?: boolean; } diff --git a/src/ts/uifactory.ts b/src/ts/uifactory.ts index 78cb36c67..3e1266a09 100644 --- a/src/ts/uifactory.ts +++ b/src/ts/uifactory.ts @@ -11,7 +11,7 @@ import { SettingsPanelPageOpenButton } from './components/settingspanelpageopenb import { SubtitleSettingsLabel } from './components/subtitlesettings/subtitlesettingslabel'; import { SubtitleSelectBox } from './components/subtitleselectbox'; import { ControlBar } from './components/controlbar'; -import { Container } from './components/container'; +import { Container, ContainerConfig } from './components/container'; import { PlaybackTimeLabel, PlaybackTimeLabelMode } from './components/playbacktimelabel'; import { SeekBar } from './components/seekbar'; import { SeekBarLabel } from './components/seekbarlabel'; @@ -50,9 +50,9 @@ import { AudioTrackListBox } from './components/audiotracklistbox'; import { SpatialNavigation } from './spatialnavigation/spatialnavigation'; import { RootNavigationGroup } from './spatialnavigation/rootnavigationgroup'; import { ListNavigationGroup, ListOrientation } from './spatialnavigation/ListNavigationGroup'; +import { EcoModeContainer } from './components/ecomodecontainer'; export namespace UIFactory { - export function buildDefaultUI(player: PlayerAPI, config: UIConfig = {}): UIManager { return UIFactory.buildModernUI(player, config); } @@ -69,22 +69,35 @@ export namespace UIFactory { return UIFactory.buildModernTvUI(player, config); } - export function modernUI() { + export function modernUI(config: UIConfig) { let subtitleOverlay = new SubtitleOverlay(); - let mainSettingsPanelPage = new SettingsPanelPage({ - components: [ - new SettingsPanelItem(i18n.getLocalizer('settings.video.quality'), new VideoQualitySelectBox()), - new SettingsPanelItem(i18n.getLocalizer('speed'), new PlaybackSpeedSelectBox()), - new SettingsPanelItem(i18n.getLocalizer('settings.audio.track'), new AudioTrackSelectBox()), - new SettingsPanelItem(i18n.getLocalizer('settings.audio.quality'), new AudioQualitySelectBox()), - ], + let mainSettingsPanelPage: SettingsPanelPage; + + const components: Container[] = [ + new SettingsPanelItem(i18n.getLocalizer('settings.video.quality'), new VideoQualitySelectBox()), + new SettingsPanelItem(i18n.getLocalizer('speed'), new PlaybackSpeedSelectBox()), + new SettingsPanelItem(i18n.getLocalizer('settings.audio.track'), new AudioTrackSelectBox()), + new SettingsPanelItem(i18n.getLocalizer('settings.audio.quality'), new AudioQualitySelectBox()), + ]; + + if (config.ecoMode) { + const ecoModeContainer = new EcoModeContainer(); + + ecoModeContainer.setOnToggleCallback(() => { + // forces the browser to re-calculate the height of the settings panel when adding/removing elements + settingsPanel.getDomElement().css({ width: '', height: '' }); + }); + + components.unshift(ecoModeContainer); + } + + mainSettingsPanelPage = new SettingsPanelPage({ + components, }); let settingsPanel = new SettingsPanel({ - components: [ - mainSettingsPanelPage, - ], + components: [mainSettingsPanelPage], hidden: true, }); @@ -112,7 +125,8 @@ export namespace UIFactory { { role: 'menubar', }, - )); + ), + ); settingsPanel.addComponent(subtitleSettingsPanelPage); @@ -121,9 +135,15 @@ export namespace UIFactory { settingsPanel, new Container({ components: [ - new PlaybackTimeLabel({ timeLabelMode: PlaybackTimeLabelMode.CurrentTime, hideInLivePlayback: true }), + new PlaybackTimeLabel({ + timeLabelMode: PlaybackTimeLabelMode.CurrentTime, + hideInLivePlayback: true, + }), new SeekBar({ label: new SeekBarLabel() }), - new PlaybackTimeLabel({ timeLabelMode: PlaybackTimeLabelMode.TotalTime, cssClasses: ['text-right'] }), + new PlaybackTimeLabel({ + timeLabelMode: PlaybackTimeLabelMode.TotalTime, + cssClasses: ['text-right'], + }), ], cssClasses: ['controlbar-top'], }), @@ -173,10 +193,7 @@ export namespace UIFactory { new AdClickOverlay(), new PlaybackToggleOverlay(), new Container({ - components: [ - new AdMessageLabel({ text: i18n.getLocalizer('ads.remainingTime')}), - new AdSkipButton(), - ], + components: [new AdMessageLabel({ text: i18n.getLocalizer('ads.remainingTime') }), new AdSkipButton()], cssClass: 'ui-ads-status', }), new ControlBar({ @@ -217,9 +234,7 @@ export namespace UIFactory { }); let settingsPanel = new SettingsPanel({ - components: [ - mainSettingsPanelPage, - ], + components: [mainSettingsPanelPage], hidden: true, pageTransitionAnimation: false, hideDelay: -1, @@ -249,7 +264,8 @@ export namespace UIFactory { { role: 'menubar', }, - )); + ), + ); settingsPanel.addComponent(subtitleSettingsPanelPage); @@ -260,9 +276,15 @@ export namespace UIFactory { components: [ new Container({ components: [ - new PlaybackTimeLabel({ timeLabelMode: PlaybackTimeLabelMode.CurrentTime, hideInLivePlayback: true }), + new PlaybackTimeLabel({ + timeLabelMode: PlaybackTimeLabelMode.CurrentTime, + hideInLivePlayback: true, + }), new SeekBar({ label: new SeekBarLabel() }), - new PlaybackTimeLabel({ timeLabelMode: PlaybackTimeLabelMode.TotalTime, cssClasses: ['text-right'] }), + new PlaybackTimeLabel({ + timeLabelMode: PlaybackTimeLabelMode.TotalTime, + cssClasses: ['text-right'], + }), ], cssClasses: ['controlbar-top'], }), @@ -317,10 +339,7 @@ export namespace UIFactory { ], }), new Container({ - components: [ - new AdMessageLabel({ text: 'Ad: {remainingTime} secs' }), - new AdSkipButton(), - ], + components: [new AdMessageLabel({ text: 'Ad: {remainingTime} secs' }), new AdSkipButton()], cssClass: 'ui-ads-status', }), ], @@ -339,9 +358,15 @@ export namespace UIFactory { components: [ new Container({ components: [ - new PlaybackTimeLabel({ timeLabelMode: PlaybackTimeLabelMode.CurrentTime, hideInLivePlayback: true }), + new PlaybackTimeLabel({ + timeLabelMode: PlaybackTimeLabelMode.CurrentTime, + hideInLivePlayback: true, + }), new SeekBar({ smoothPlaybackPositionUpdateIntervalMs: -1 }), - new PlaybackTimeLabel({ timeLabelMode: PlaybackTimeLabelMode.TotalTime, cssClasses: ['text-right'] }), + new PlaybackTimeLabel({ + timeLabelMode: PlaybackTimeLabelMode.TotalTime, + cssClasses: ['text-right'], + }), ], cssClasses: ['controlbar-top'], }), @@ -372,43 +397,64 @@ export namespace UIFactory { // show smallScreen UI only on mobile/handheld devices let smallScreenSwitchWidth = 600; - return new UIManager(player, [{ - ui: modernSmallScreenAdsUI(), - condition: (context: UIConditionContext) => { - return context.isMobile && context.documentWidth < smallScreenSwitchWidth && context.isAd - && context.adRequiresUi; - }, - }, { - ui: modernAdsUI(), - condition: (context: UIConditionContext) => { - return context.isAd && context.adRequiresUi; - }, - }, { - ui: modernSmallScreenUI(), - condition: (context: UIConditionContext) => { - return !context.isAd && !context.adRequiresUi && context.isMobile - && context.documentWidth < smallScreenSwitchWidth; - }, - }, { - ui: modernUI(), - condition: (context: UIConditionContext) => { - return !context.isAd && !context.adRequiresUi; - }, - }], config); + return new UIManager( + player, + [ + { + ui: modernSmallScreenAdsUI(), + condition: (context: UIConditionContext) => { + return ( + context.isMobile && context.documentWidth < smallScreenSwitchWidth && context.isAd && context.adRequiresUi + ); + }, + }, + { + ui: modernAdsUI(), + condition: (context: UIConditionContext) => { + return context.isAd && context.adRequiresUi; + }, + }, + { + ui: modernSmallScreenUI(), + condition: (context: UIConditionContext) => { + return ( + !context.isAd && + !context.adRequiresUi && + context.isMobile && + context.documentWidth < smallScreenSwitchWidth + ); + }, + }, + { + ui: modernUI(config), + condition: (context: UIConditionContext) => { + return !context.isAd && !context.adRequiresUi; + }, + }, + ], + config, + ); } export function buildModernSmallScreenUI(player: PlayerAPI, config: UIConfig = {}): UIManager { - return new UIManager(player, [{ - ui: modernSmallScreenAdsUI(), - condition: (context: UIConditionContext) => { - return context.isAd && context.adRequiresUi; - }, - }, { - ui: modernSmallScreenUI(), - condition: (context: UIConditionContext) => { - return !context.isAd && !context.adRequiresUi; - }, - }], config); + return new UIManager( + player, + [ + { + ui: modernSmallScreenAdsUI(), + condition: (context: UIConditionContext) => { + return context.isAd && context.adRequiresUi; + }, + }, + { + ui: modernSmallScreenUI(), + condition: (context: UIConditionContext) => { + return !context.isAd && !context.adRequiresUi; + }, + }, + ], + config, + ); } export function buildModernCastReceiverUI(player: PlayerAPI, config: UIConfig = {}): UIManager { @@ -416,9 +462,15 @@ export namespace UIFactory { } export function buildModernTvUI(player: PlayerAPI, config: UIConfig = {}): UIManager { - return new UIManager(player, [{ + return new UIManager( + player, + [ + { ...modernTvUI(), - }], config); + }, + ], + config, + ); } export function modernTvUI() { @@ -426,9 +478,7 @@ export namespace UIFactory { const subtitleListPanel = new SettingsPanel({ components: [ new SettingsPanelPage({ - components: [ - new SettingsPanelItem(null, subtitleListBox), - ], + components: [new SettingsPanelItem(null, subtitleListBox)], }), ], hidden: true, @@ -438,9 +488,7 @@ export namespace UIFactory { const audioTrackListPanel = new SettingsPanel({ components: [ new SettingsPanelPage({ - components: [ - new SettingsPanelItem(null, audioTrackListBox), - ], + components: [new SettingsPanelItem(null, audioTrackListBox)], }), ], hidden: true, @@ -470,9 +518,15 @@ export namespace UIFactory { components: [ new Container({ components: [ - new PlaybackTimeLabel({ timeLabelMode: PlaybackTimeLabelMode.CurrentTime, hideInLivePlayback: true }), + new PlaybackTimeLabel({ + timeLabelMode: PlaybackTimeLabelMode.CurrentTime, + hideInLivePlayback: true, + }), seekBar, - new PlaybackTimeLabel({ timeLabelMode: PlaybackTimeLabelMode.RemainingTime, cssClasses: ['text-right'] }), + new PlaybackTimeLabel({ + timeLabelMode: PlaybackTimeLabelMode.RemainingTime, + cssClasses: ['text-right'], + }), ], cssClasses: ['controlbar-top'], }), diff --git a/src/ts/uimanager.ts b/src/ts/uimanager.ts index cefe369e6..77e60fca8 100644 --- a/src/ts/uimanager.ts +++ b/src/ts/uimanager.ts @@ -1,6 +1,6 @@ import {UIContainer} from './components/uicontainer'; import {DOM} from './dom'; -import {Component, ComponentConfig} from './components/component'; +import { Component, ComponentConfig, ViewModeChangedEventArgs } from './components/component'; import {Container} from './components/container'; import { SeekBar, SeekBarMarker } from './components/seekbar'; import {NoArgs, EventDispatcher, CancelEventArgs} from './eventdispatcher'; @@ -17,6 +17,9 @@ import { SpatialNavigation } from './spatialnavigation/spatialnavigation'; import { SubtitleSettingsManager } from './components/subtitlesettings/subtitlesettingsmanager'; import { StorageUtils } from './storageutils'; +/** + * @category Configs + */ export interface LocalizationConfig { /** * Sets the desired language, and falls back to 'en' if there is no vocabulary for the desired language. Setting it @@ -30,6 +33,9 @@ export interface LocalizationConfig { vocabularies?: Vocabularies; } +/** + * @category Configs + */ export interface InternalUIConfig extends UIConfig { events: { /** @@ -621,6 +627,7 @@ export class UIInstanceManager { onSeeked: new EventDispatcher(), onComponentShow: new EventDispatcher, NoArgs>(), onComponentHide: new EventDispatcher, NoArgs>(), + onComponentViewModeChanged: new EventDispatcher, ViewModeChangedEventArgs>(), onControlsShow: new EventDispatcher(), onPreviewControlsHide: new EventDispatcher(), onControlsHide: new EventDispatcher(), @@ -731,6 +738,10 @@ export class UIInstanceManager { return this.events.onRelease; } + get onComponentViewModeChanged(): EventDispatcher, ViewModeChangedEventArgs> { + return this.events.onComponentViewModeChanged; + } + protected clearEventHandlers(): void { this.playerWrapper.clearEventHandlers(); @@ -840,6 +851,8 @@ export interface WrappedPlayer extends PlayerAPI { /** * Wraps the player to track event handlers and provide a simple method to remove all registered event * handlers from the player. + * + * @category Utils */ export class PlayerWrapper { diff --git a/src/ts/uiutils.ts b/src/ts/uiutils.ts index 7798985e0..85d518c18 100644 --- a/src/ts/uiutils.ts +++ b/src/ts/uiutils.ts @@ -1,6 +1,9 @@ import {Component, ComponentConfig} from './components/component'; import {Container} from './components/container'; +/** + * @category Utils + */ export namespace UIUtils { export interface TreeTraversalCallback { (component: Component, parent?: Component): void; diff --git a/src/ts/vttutils.ts b/src/ts/vttutils.ts index 8e10d4c9b..21d85743b 100644 --- a/src/ts/vttutils.ts +++ b/src/ts/vttutils.ts @@ -206,6 +206,9 @@ const setCssForEndLineAlign = ( cueContainerDom.css(opositeToOverlayReferenceEdge, `${100 - offset}%`); }; +/** + * @category Utils + */ export namespace VttUtils { export const setVttCueBoxStyles = ( cueContainer: SubtitleLabel, diff --git a/typedoc.json b/typedoc.json new file mode 100644 index 000000000..e39dea38d --- /dev/null +++ b/typedoc.json @@ -0,0 +1,23 @@ +{ + "out": "./docs/", + "entryPoints": [ + "src/ts/main.ts" + ], + "tsconfig": "./tsconfig.json", + "defaultCategory": "UI Framework", + "navigation": { + "includeCategories": true, + "includeGroups": true + }, + "excludeProtected": true, + "categoryOrder": [ + "UI Framework", + "Configs", + "Components", + "Buttons", + "Labels", + "Containers", + "Localization", + "Utils" + ] +}