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/scripts/updateChangelog.js b/.github/scripts/updateChangelog.js index 8a34b0e5e..b04a0bf0a 100644 --- a/.github/scripts/updateChangelog.js +++ b/.github/scripts/updateChangelog.js @@ -6,12 +6,19 @@ * @param {string} releaseDate the release date to be written to the changelog */ function updateChangeLog(changelogString, version, releaseDate) { - const optionalBetaOrRc = '(-rc.d+)?(-(b|beta).d+)?'; - const changelogVersionRegExp = new RegExp( - `\\[(development|develop|unreleased|${version})${optionalBetaOrRc}.*`, - 'gi', - ); - return changelogString.replace(changelogVersionRegExp, `[${version}] - ${releaseDate}`); - } - - module.exports.updateChangeLog = updateChangeLog; + const optionalBetaOrRc = '(-rc.d+)?(-(b|beta).d+)?'; + const changelogVersionRegExp = new RegExp( + `\\[(development|develop|unreleased|${version})${optionalBetaOrRc}.*`, + 'gi', + ); + + const lastReleaseVersion = changelogString.match(/## \[(\d+.\d+.\d+)\] - \d{4}-\d{2}-\d{2}/)[1]; + const changelogWithReleaseVersionAndDate = changelogString.replace(changelogVersionRegExp, `[${version}] - ${releaseDate}`); + + return changelogWithReleaseVersionAndDate.replace( + '## 1.0.0 (2017-02-03)\n- First release\n\n', + `## 1.0.0 (2017-02-03)\n- First release\n\n[${version}]: https://github.com/bitmovin/bitmovin-player-ui/compare/v${lastReleaseVersion}...v${version}\n` + ); +} + +module.exports.updateChangeLog = updateChangeLog; 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/.github/workflows/tag-release-version.yml b/.github/workflows/tag-release-version.yml index 1a703db89..da44e62bc 100644 --- a/.github/workflows/tag-release-version.yml +++ b/.github/workflows/tag-release-version.yml @@ -1,7 +1,7 @@ name: Trigger release on merge run-name: Starting release for ${{ github.actor }} PR merge on: - pull_request: + pull_request_target: types: - closed branches: 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 219125dbc..55db7cd03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,88 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [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 + +### Added +- Type export for `UIVariant` interface + +## [3.59.0] - 2024-04-12 + +### Added +- `UIContainerConfig.hideImmediatelyOnMouseLeave` to immediately hide the UI when mouse leaves it + +### Fixed +- Triggering UI release after merging a PR from a fork + +## [3.58.0] - 2024-04-08 + +### Added +- `UIConfig#seekbarSnappingEnabled` config option to enable/disable the play head snapping to markers on the seek bar when seeking near them. Default is `true`. + +## [3.57.0] - 2024-03-28 + +### Added +- `disableStorageApi` config option + +### Fixed +- Subtitle settings not being retained when the UI variant switches + +## [3.56.0] - 2024-03-26 + +### Changed +- `localStorage` availability check to not create a test-entry anymore + +## [3.55.0] - 2024-03-21 + +### Added +- Automatically add compare link in changelog file in relase workflow + +### Fixed +- Missing compare link in the changelog file + ## [3.54.0] - 2024-02-01 ### Fixed @@ -898,6 +980,23 @@ 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.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 +[3.57.0]: https://github.com/bitmovin/bitmovin-player-ui/compare/v3.56.0...v3.57.0 +[3.56.0]: https://github.com/bitmovin/bitmovin-player-ui/compare/v3.55.0...v3.56.0 +[3.55.0]: https://github.com/bitmovin/bitmovin-player-ui/compare/v3.54.0...v3.55.0 +[3.54.0]: https://github.com/bitmovin/bitmovin-player-ui/compare/v3.53.0...v3.54.0 +[3.53.0]: https://github.com/bitmovin/bitmovin-player-ui/compare/v3.52.2...v3.53.0 [3.52.2]: https://github.com/bitmovin/bitmovin-player-ui/compare/v3.52.1...v3.52.2 [3.52.1]: https://github.com/bitmovin/bitmovin-player-ui/compare/v3.52.0...v3.52.1 [3.52.0]: https://github.com/bitmovin/bitmovin-player-ui/compare/v3.51.0...v3.52.0 diff --git a/README.md b/README.md index 98b4c8279..3e61d1c15 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). ## 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 05f140758..9364db661 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,19 @@ { "name": "bitmovin-player-ui", - "version": "3.54.0", + "version": "3.69.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "bitmovin-player-ui", - "version": "3.54.0", + "version": "3.69.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 45773c097..9e907d99c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bitmovin-player-ui", - "version": "3.54.0", + "version": "3.69.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/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..f71f90cdd 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 { /** @@ -74,6 +76,8 @@ export interface ComponentHoverChangedEventArgs extends NoArgs { /** * The base class of the UI framework. * Each component must extend this class and optionally the config interface. + * + * @category Components */ export class Component { @@ -272,7 +276,7 @@ export class Component { 'id': this.config.id, 'class': this.getCssClasses(), 'role': this.config.role, - }); + }, this); return element; } diff --git a/src/ts/components/container.ts b/src/ts/components/container.ts index f9337dac2..ea426b01f 100644 --- a/src/ts/components/container.ts +++ b/src/ts/components/container.ts @@ -5,6 +5,8 @@ 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 { @@ -121,7 +125,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, { 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 cf62ef002..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 { /** @@ -44,7 +46,13 @@ export interface SeekBarConfig extends ComponentConfig { keyStepIncrements?: { leftRight: number, upDown: number }; /** - * Used for seekBar marker snapping range percentage + * Used to enable/disable snapping to markers on the seek bar when seeking near them. + * Default: true + */ + snappingEnabled?: boolean; + + /** + * Defines tolerance for snapping to markers, if snapping to seek bar markers is enabled. */ snappingRange?: number; @@ -64,6 +72,9 @@ export interface SeekPreviewEventArgs extends SeekPreviewArgs { scrubbing: boolean; } +/** + * @category Components + */ export interface SeekBarMarker { marker: TimelineMarker; position: number; @@ -79,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 { @@ -151,6 +164,7 @@ export class SeekBar extends Component { tabIndex: 0, snappingRange: 1, enableSeekPreview: true, + snappingEnabled: true, }, this.config); this.label = this.config.label; @@ -452,6 +466,10 @@ export class SeekBar extends Component { this.config.snappingRange = uimanager.getConfig().seekbarSnappingRange; } + if (typeof uimanager.getConfig().seekbarSnappingEnabled === 'boolean') { + this.config.snappingEnabled = uimanager.getConfig().seekbarSnappingEnabled; + } + // Initialize seekbar playbackPositionHandler(); // Set the playback position this.setBufferPosition(0); @@ -636,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'), @@ -705,13 +723,17 @@ export class SeekBar extends Component { new DOM(document).off('touchend mouseup', mouseTouchUpHandler); let targetPercentage = 100 * this.getOffset(e); - let snappedChapter = this.timelineMarkersHandler && this.timelineMarkersHandler.getMarkerAtPosition(targetPercentage); + + if (this.config.snappingEnabled) { + const matchingMarker = this.timelineMarkersHandler?.getMarkerAtPosition(targetPercentage); + targetPercentage = matchingMarker ? matchingMarker.position : targetPercentage; + } this.setSeeking(false); seeking = false; // Fire seeked event - this.onSeekedEvent(snappedChapter ? snappedChapter.position : targetPercentage); + this.onSeekedEvent(targetPercentage); }; // A seek always start with a touchstart or mousedown directly on the 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..81cfa6c80 100644 --- a/src/ts/components/selectbox.ts +++ b/src/ts/components/selectbox.ts @@ -12,6 +12,8 @@ import { i18n, LocalizableText } from '../localization/i18n'; * ... * * + * + * @category Components */ export class SelectBox extends ListSelector { @@ -31,7 +33,7 @@ export class SelectBox extends ListSelector { 'id': this.config.id, 'class': this.getCssClasses(), 'aria-label': i18n.performLocalization(this.config.ariaLabel), - }); + }, this); this.selectElement = selectElement; this.updateDomItems(); diff --git a/src/ts/components/settingspanel.ts b/src/ts/components/settingspanel.ts index c09dbb990..a6c1baa05 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 { @@ -162,7 +166,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()) { 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 2222988e4..e6448d56f 100644 --- a/src/ts/components/subtitlesettings/subtitlesettingselectbox.ts +++ b/src/ts/components/subtitlesettings/subtitlesettingselectbox.ts @@ -2,25 +2,30 @@ import {SubtitleOverlay} from '../subtitleoverlay'; import {ListSelectorConfig} from '../listselector'; import {SelectBox} from '../selectbox'; import {SubtitleSettingsManager} from './subtitlesettingsmanager'; +import { PlayerAPI } from 'bitmovin-player'; +import { UIInstanceManager } from '../../uimanager'; +/** + * @category Configs + */ export interface SubtitleSettingSelectBoxConfig extends ListSelectorConfig { overlay: SubtitleOverlay; - settingsManager: SubtitleSettingsManager; } /** * Base class for all subtitles settings select box + * + * @category Components **/ export class SubtitleSettingSelectBox extends SelectBox { - protected settingsManager: SubtitleSettingsManager; + protected settingsManager?: SubtitleSettingsManager; protected overlay: SubtitleOverlay; private currentCssClass: string; constructor(config: SubtitleSettingSelectBoxConfig) { super(config); - this.settingsManager = config.settingsManager; this.overlay = config.overlay; } @@ -41,4 +46,8 @@ export class SubtitleSettingSelectBox extends SelectBox { this.overlay.getDomElement().addClass(this.currentCssClass); } } + + configure(player: PlayerAPI, uimanager: UIInstanceManager): void { + this.settingsManager = uimanager.getSubtitleSettingsManager(); + } } 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 09689083b..27432ecbe 100644 --- a/src/ts/components/subtitlesettings/subtitlesettingsmanager.ts +++ b/src/ts/components/subtitlesettings/subtitlesettingsmanager.ts @@ -1,6 +1,6 @@ -import {StorageUtils} from '../../storageutils'; -import {Component, ComponentConfig} from '../component'; -import {EventDispatcher, Event} from '../../eventdispatcher'; +import { StorageUtils } from '../../storageutils'; +import { Component, ComponentConfig } from '../component'; +import { EventDispatcher, Event } from '../../eventdispatcher'; interface SubtitleSettings { fontColor?: string; @@ -18,8 +18,10 @@ interface Properties { [name: string]: SubtitleSettingsProperty; } +/** + * @category Utils + */ export class SubtitleSettingsManager { - private userSettings: SubtitleSettings; private localStorageKey: string; @@ -37,23 +39,8 @@ export class SubtitleSettingsManager { constructor() { this.userSettings = {}; - this.localStorageKey = DummyComponent.instance().prefixCss('subtitlesettings'); - - for (let propertyName in this._properties) { - this._properties[propertyName].onChanged.subscribe((sender, property) => { - if (property.isSet()) { - (this.userSettings)[propertyName] = property.value; - } else { - // Delete the property from the settings object if unset to avoid serialization of null values - delete (this.userSettings)[propertyName]; - } - - // Save the settings object when a property has changed - this.save(); - }); - } - - this.load(); + this.localStorageKey = + DummyComponent.instance().prefixCss('subtitlesettings'); } public reset(): void { @@ -98,6 +85,24 @@ export class SubtitleSettingsManager { return this._properties.windowOpacity; } + public initialize() { + for (let propertyName in this._properties) { + this._properties[propertyName].onChanged.subscribe((sender, property) => { + if (property.isSet()) { + (this.userSettings)[propertyName] = property.value; + } else { + // Delete the property from the settings object if unset to avoid serialization of null values + delete (this.userSettings)[propertyName]; + } + + // Save the settings object when a property has changed + this.save(); + }); + } + + this.load(); + } + /** * Saves the settings to local storage. */ @@ -109,7 +114,8 @@ export class SubtitleSettingsManager { * Loads the settings from local storage */ public load(): void { - this.userSettings = StorageUtils.getObject(this.localStorageKey) || {}; + this.userSettings = + StorageUtils.getObject(this.localStorageKey) || {}; // Apply the loaded settings for (let property in this.userSettings) { @@ -123,7 +129,6 @@ export class SubtitleSettingsManager { * {@link SubtitleSettingsManager}. */ class DummyComponent extends Component { - private static _instance: DummyComponent; public static instance(): DummyComponent { @@ -140,14 +145,19 @@ class DummyComponent extends Component { } export class SubtitleSettingsProperty { - private _manager: SubtitleSettingsManager; - private _onChanged: EventDispatcher>; + private _onChanged: EventDispatcher< + SubtitleSettingsManager, + SubtitleSettingsProperty + >; private _value: T; constructor(manager: SubtitleSettingsManager) { this._manager = manager; - this._onChanged = new EventDispatcher>(); + this._onChanged = new EventDispatcher< + SubtitleSettingsManager, + SubtitleSettingsProperty + >(); } public isSet(): boolean { @@ -176,7 +186,10 @@ export class SubtitleSettingsProperty { this._onChanged.dispatch(this._manager, this); } - public get onChanged(): Event> { + public get onChanged(): Event< + SubtitleSettingsManager, + SubtitleSettingsProperty + > { return this._onChanged.getEvent(); } -} \ No newline at end of file +} diff --git a/src/ts/components/subtitlesettings/subtitlesettingspanelpage.ts b/src/ts/components/subtitlesettings/subtitlesettingspanelpage.ts index 605556b01..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; @@ -36,43 +42,40 @@ export class SubtitleSettingsPanelPage extends SettingsPanelPage { this.overlay = config.overlay; this.settingsPanel = config.settingsPanel; - let manager = new SubtitleSettingsManager(); this.config = this.mergeConfig(config, { components: []>[ new SettingsPanelItem(i18n.getLocalizer('settings.subtitles.font.size'), new FontSizeSelectBox({ - overlay: this.overlay, settingsManager: manager, + overlay: this.overlay, })), new SettingsPanelItem(i18n.getLocalizer('settings.subtitles.font.family'), new FontFamilySelectBox({ - overlay: this.overlay, settingsManager: manager, + overlay: this.overlay, })), new SettingsPanelItem(i18n.getLocalizer('settings.subtitles.font.color'), new FontColorSelectBox({ - overlay: this.overlay, settingsManager: manager, + overlay: this.overlay, })), new SettingsPanelItem(i18n.getLocalizer('settings.subtitles.font.opacity'), new FontOpacitySelectBox({ - overlay: this.overlay, settingsManager: manager, + overlay: this.overlay, })), new SettingsPanelItem(i18n.getLocalizer('settings.subtitles.characterEdge'), new CharacterEdgeSelectBox({ - overlay: this.overlay, settingsManager: manager, + overlay: this.overlay, })), new SettingsPanelItem(i18n.getLocalizer('settings.subtitles.background.color'), new BackgroundColorSelectBox({ - overlay: this.overlay, settingsManager: manager, + overlay: this.overlay, })), new SettingsPanelItem(i18n.getLocalizer('settings.subtitles.background.opacity'), new BackgroundOpacitySelectBox({ - overlay: this.overlay, settingsManager: manager, + overlay: this.overlay, })), new SettingsPanelItem(i18n.getLocalizer('settings.subtitles.window.color'), new WindowColorSelectBox({ - overlay: this.overlay, settingsManager: manager, + overlay: this.overlay, })), new SettingsPanelItem(i18n.getLocalizer('settings.subtitles.window.opacity'), new WindowOpacitySelectBox({ - overlay: this.overlay, settingsManager: manager, + overlay: this.overlay, })), new SettingsPanelItem(new SettingsPanelPageBackButton({ container: this.settingsPanel, text: i18n.getLocalizer('back'), - }), new SubtitleSettingsResetButton({ - settingsManager: manager, - }), { + }), new SubtitleSettingsResetButton({}), { role: 'menubar', }), ], diff --git a/src/ts/components/subtitlesettings/subtitlesettingsresetbutton.ts b/src/ts/components/subtitlesettings/subtitlesettingsresetbutton.ts index 3ff04d9e6..0012e6b5d 100644 --- a/src/ts/components/subtitlesettings/subtitlesettingsresetbutton.ts +++ b/src/ts/components/subtitlesettings/subtitlesettingsresetbutton.ts @@ -4,16 +4,16 @@ import {Button, ButtonConfig} from '../button'; import { PlayerAPI } from 'bitmovin-player'; import { i18n } from '../../localization/i18n'; -export interface SubtitleSettingsResetButtonConfig extends ButtonConfig { - settingsManager: SubtitleSettingsManager; -} - /** * A button that resets all subtitle settings to their defaults. + * + * @category Buttons */ export class SubtitleSettingsResetButton extends Button { - constructor(config: SubtitleSettingsResetButtonConfig) { + private settingsManager: SubtitleSettingsManager; + + constructor(config: ButtonConfig) { super(config); this.config = this.mergeConfig(config, { @@ -24,9 +24,10 @@ export class SubtitleSettingsResetButton extends Button { configure(player: PlayerAPI, uimanager: UIInstanceManager): void { super.configure(player, uimanager); + this.settingsManager = uimanager.getSubtitleSettingsManager(); this.onClick.subscribe(() => { - (this.config).settingsManager.reset(); + this.settingsManager.reset(); }); } } 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 cfcd60b71..f47f8a111 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 { DOM, HTMLElementWithComponent } from '../dom'; import {Timeout} from '../timeout'; import {PlayerUtils} from '../playerutils'; import { CancelEventArgs, 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 { /** @@ -27,11 +30,20 @@ export interface UIContainerConfig extends ContainerConfig { * Default: the UI container itself */ userInteractionEventSource?: HTMLElement; + + /** + * Specify whether the UI should be hidden immediatly if the mouse leaves the userInteractionEventSource. + * If false or not set it will wait for the hideDelay. + * Default: false + */ + hideImmediatelyOnMouseLeave?: boolean; } /** * 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 { @@ -60,6 +72,7 @@ export class UIContainer extends Container { role: 'region', ariaLabel: i18n.getLocalizer('player'), hideDelay: 5000, + hideImmediatelyOnMouseLeave: false, }, this.config); this.playerStateChange = new EventDispatcher(); @@ -134,6 +147,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 @@ -142,7 +176,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(); } @@ -176,7 +217,11 @@ export class UIContainer extends Container { // 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()) { - this.uiHideTimeout.start(); + if (this.config.hideImmediatelyOnMouseLeave) { + this.hideUi(); + } else { + this.uiHideTimeout.start(); + } } }, }]; 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..f178fd893 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); } 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..9d35ca5bf 100644 --- a/src/ts/localization/i18n.ts +++ b/src/ts/localization/i18n.ts @@ -14,10 +14,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 +96,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 +211,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/main.ts b/src/ts/main.ts index c083b69c7..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 } 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 @@ -110,4 +113,4 @@ if (typeof Object.assign !== 'function') { } return target; }; -} \ No newline at end of file +} 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 358186b8f..34e06b2c0 100644 --- a/src/ts/storageutils.ts +++ b/src/ts/storageutils.ts @@ -1,36 +1,26 @@ +import { UIConfig } from './uiconfig'; + +/** + * @category Utils + */ export namespace StorageUtils { - let hasLocalStorageCache: boolean; + let disableStorageApi: boolean; - export function hasLocalStorage(): boolean { - if (hasLocalStorageCache) { - return hasLocalStorageCache; - } + export function setStorageApiDisabled(uiConfig: UIConfig) { + disableStorageApi = uiConfig.disableStorageApi; + } - // hasLocalStorage is used to safely ensure we can use localStorage - // taken from https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API#Feature-detecting_localStorage - let storage: any = { length: 0 }; + function shouldUseLocalStorage(): boolean { try { - storage = window['localStorage']; - let x = '__storage_test__'; - storage.setItem(x, x); - storage.removeItem(x); - hasLocalStorageCache = true; + return ( + !disableStorageApi && + window.localStorage && + typeof localStorage.getItem === 'function' && + typeof localStorage.setItem === 'function' + ); + } catch (e) { + return false; } - catch (e) { - hasLocalStorageCache = e instanceof DOMException && ( - // everything except Firefox - e.code === 22 || - // Firefox - e.code === 1014 || - // test name field too, because code might not be present - // everything except Firefox - e.name === 'QuotaExceededError' || - // Firefox - e.name === 'NS_ERROR_DOM_QUOTA_REACHED') && - // acknowledge QuotaExceededError only if there's something already stored - storage.length !== 0; - } - return hasLocalStorageCache; } /** @@ -39,8 +29,12 @@ export namespace StorageUtils { * @param data the item's data */ export function setItem(key: string, data: string): void { - if (StorageUtils.hasLocalStorage()) { - window.localStorage.setItem(key, data); + if (shouldUseLocalStorage()) { + try { + window.localStorage.setItem(key, data); + } catch (e) { + console.debug(`Failed to set storage item ${key}`, e); + } } } @@ -50,11 +44,15 @@ export namespace StorageUtils { * @return {string | null} Returns the string if found, null if there is no data stored for the key */ export function getItem(key: string): string | null { - if (StorageUtils.hasLocalStorage()) { - return window.localStorage.getItem(key); - } else { - return null; + if (shouldUseLocalStorage()) { + try { + return window.localStorage.getItem(key); + } catch (e) { + console.debug(`Failed to get storage item ${key}`, e); + } } + + return null; } /** @@ -66,10 +64,8 @@ export namespace StorageUtils { * @param data the object to store */ export function setObject(key: string, data: T): void { - if (StorageUtils.hasLocalStorage()) { - let json = JSON.stringify(data); - setItem(key, json); - } + let json = JSON.stringify(data); + setItem(key, json); } /** @@ -80,14 +76,12 @@ export namespace StorageUtils { * @param key the key to look up its associated object * @return {any} Returns the object if found, null otherwise */ - export function getObject(key: string): T { - if (StorageUtils.hasLocalStorage()) { - let json = getItem(key); + export function getObject(key: string): T | null { + let json = getItem(key); - if (key) { - let object = JSON.parse(json); - return object; - } + if (json) { + let object = JSON.parse(json); + return object; } return null; } diff --git a/src/ts/stringutils.ts b/src/ts/stringutils.ts index 3e3095a92..da9f771b5 100644 --- a/src/ts/stringutils.ts +++ b/src/ts/stringutils.ts @@ -1,6 +1,9 @@ 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'; 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/uiconfig.ts b/src/ts/uiconfig.ts index ab7da44f4..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,13 +76,29 @@ 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; /** - * Specifies the seekbar snapping range in percentage + * Whether the play head should snap to markers on the seek bar when seeking sufficiently near them. + * + * The related config option `seekbarSnappingRange` defines the tolerance that is used to determine whether a seek + * time hits a marker. + * + * Note: + * - When hitting a point marker (i.e. one without duration), the play head would snap to the exact time of the + * marker. + * - Likewise, when hitting a range marker (i.e. one with duration) which effectively snaps to the start of the time + * range that it defines. + * + * Default: true + */ + seekbarSnappingEnabled?: boolean; + /** + * Specifies the seek bar marker snapping tolerance in percent. This option has no effect if `seekbarSnappingEnabled` + * is set to false. * Default: 1 */ seekbarSnappingRange?: number; @@ -100,4 +122,13 @@ export interface UIConfig { * Forces subtitle-labels back into their respective container if they overflow and are therefore cropped. */ forceSubtitlesIntoViewContainer?: boolean; + + /** + * 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 c22938478..cccfee028 100644 --- a/src/ts/uimanager.ts +++ b/src/ts/uimanager.ts @@ -14,7 +14,12 @@ import { i18n, CustomVocabulary, Vocabularies } from './localization/i18n'; import { FocusVisibilityTracker } from './focusvisibilitytracker'; import { isMobileV3PlayerAPI, MobileV3PlayerAPI, MobileV3PlayerEvent } from './mobilev3playerapi'; 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 @@ -28,6 +33,9 @@ export interface LocalizationConfig { vocabularies?: Vocabularies; } +/** + * @category Configs + */ export interface InternalUIConfig extends UIConfig { events: { /** @@ -111,6 +119,7 @@ export class UIManager { private config: InternalUIConfig; // Conjunction of provided uiConfig and sourceConfig from the player private managerPlayerWrapper: PlayerWrapper; private focusVisibilityTracker: FocusVisibilityTracker; + private subtitleSettingsManager: SubtitleSettingsManager; private events = { onUiVariantResolve: new EventDispatcher(), @@ -154,6 +163,7 @@ export class UIManager { this.uiVariants = playerUiOrUiVariants; } + this.subtitleSettingsManager = new SubtitleSettingsManager(); this.player = player; this.managerPlayerWrapper = new PlayerWrapper(player); @@ -198,9 +208,12 @@ export class UIManager { this.config.metadata.description = playerSourceUiConfig.metadata.description || uiconfig.metadata.description; this.config.metadata.markers = playerSourceUiConfig.metadata.markers || uiconfig.metadata.markers || []; this.config.recommendations = playerSourceUiConfig.recommendations || uiconfig.recommendations || []; + + StorageUtils.setStorageApiDisabled(uiconfig); }; updateConfig(); + this.subtitleSettingsManager.initialize(); // Update the source configuration when a new source is loaded and dispatch onUpdated const updateSource = () => { @@ -242,6 +255,7 @@ export class UIManager { player, uiVariant.ui, this.config, + this.subtitleSettingsManager, uiVariant.spatialNavigation, )); } @@ -375,6 +389,10 @@ export class UIManager { i18n.setConfig(localizationConfig); } + getSubtitleSettingsManager() { + return this.subtitleSettingsManager; + } + getConfig(): UIConfig { return this.config; } @@ -493,6 +511,7 @@ export class UIManager { // When the UI is loaded after a source was loaded, we need to tell the components to initialize themselves if (player.getSource()) { this.config.events.onUpdated.dispatch(this); + } // Fire onConfigured after UI DOM elements are successfully added. When fired immediately, the DOM elements @@ -598,6 +617,7 @@ export class UIInstanceManager { private playerWrapper: PlayerWrapper; private ui: UIContainer; private config: InternalUIConfig; + private subtitleSettingsManager: SubtitleSettingsManager; protected spatialNavigation?: SpatialNavigation; private events = { @@ -613,13 +633,18 @@ export class UIInstanceManager { onRelease: new EventDispatcher(), }; - constructor(player: PlayerAPI, ui: UIContainer, config: InternalUIConfig, spatialNavigation?: SpatialNavigation) { + constructor(player: PlayerAPI, ui: UIContainer, config: InternalUIConfig, subtitleSettingsManager: SubtitleSettingsManager, spatialNavigation?: SpatialNavigation) { this.playerWrapper = new PlayerWrapper(player); this.ui = ui; this.config = config; + this.subtitleSettingsManager = subtitleSettingsManager; this.spatialNavigation = spatialNavigation; } + getSubtitleSettingsManager() { + return this.subtitleSettingsManager; + } + getConfig(): InternalUIConfig { return this.config; } @@ -821,6 +846,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" + ] +}