├── .github ├── ISSUE_TEMPLATE │ └── config.yml ├── PULL_REQUEST_TEMPLATE.md ├── scripts │ ├── defineVersion.js │ ├── notifySlackTeam.js │ └── updateChangelog.js └── workflows │ ├── ci.yml │ ├── release.yml │ └── tag-release-version.yml ├── .gitignore ├── .husky └── pre-commit ├── .npmignore ├── .nvmrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── assets └── skin-modern │ └── images │ ├── airplay.svg │ ├── airplayX.svg │ ├── audio-tracks.svg │ ├── audio-tracksX.svg │ ├── chromecast.svg │ ├── chromecastX.svg │ ├── close.svg │ ├── closeX.svg │ ├── dot-play.svg │ ├── dot-volume.svg │ ├── fullscreen.svg │ ├── fullscreenX.svg │ ├── glasses.svg │ ├── glassesX.svg │ ├── leaf.svg │ ├── loader.svg │ ├── logo.svg │ ├── music-low.svg │ ├── music-lowX.svg │ ├── music-off.svg │ ├── music-offX.svg │ ├── music-on.svg │ ├── music-onX.svg │ ├── next.svg │ ├── nextX.svg │ ├── pause.svg │ ├── pauseX.svg │ ├── picinpic1.svg │ ├── picinpic1X.svg │ ├── picinpic2.svg │ ├── picinpic2X.svg │ ├── play.svg │ ├── playX.svg │ ├── play_big.svg │ ├── play_bigX.svg │ ├── playlist.svg │ ├── playlistX.svg │ ├── prev.svg │ ├── prevX.svg │ ├── quickseek-fastforward.svg │ ├── quickseek-rewind.svg │ ├── replay-nocircle.svg │ ├── replay.svg │ ├── replayX.svg │ ├── settings.svg │ ├── settingsX.svg │ ├── stop.svg │ ├── stopX.svg │ ├── subtitles.svg │ ├── subtitlesX.svg │ ├── toggleOff.svg │ └── toggleOn.svg ├── eslint.config.mjs ├── gulpfile.js ├── jest.config.js ├── package-lock.json ├── package.json ├── publish.sh ├── setup-jest.ts ├── spec ├── audioutils.spec.ts ├── browserutils.spec.ts ├── components │ ├── errormessageoverlay.spec.ts │ ├── fullscreentogglebutton.spec.ts │ ├── listselector.spec.ts │ ├── pictureinpicturetogglebutton.spec.ts │ ├── playbacktimelabel.spec.ts │ ├── seekbar.spec.ts │ ├── seekbarlabel.spec.ts │ ├── selectbox.spec.ts │ ├── settingspanel.spec.ts │ ├── subtitleoverlay.spec.ts │ ├── timelinemarkershandler.spec.ts │ ├── togglebutton.spec.ts │ └── uicontainer.spec.ts ├── errorutils.spec.ts ├── focusvisibilitytracker.spec.ts ├── helper │ ├── MockHelper.ts │ ├── PlayerEventEmitter.ts │ ├── mockClass.ts │ └── mockComponent.ts ├── localization.spec.ts ├── mobilev3playerapi.spec.ts ├── playerutils.spec.ts ├── release │ └── defineVersion.spec.ts ├── spatialnavigation │ ├── gethtmlelementsfromcomponents.spec.ts │ ├── keymap.spec.ts │ ├── listnavigationgroup.spec.ts │ ├── navigationalgorithm.spec.ts │ ├── navigationgroup.spec.ts │ ├── nodeeventsubscriber.spec.ts │ ├── rootnavigationgroup.spec.ts │ ├── seekbarhandler.spec.ts │ └── spatialnavigation.spec.ts ├── subtitleutils.spec.ts ├── uimanager.spec.ts ├── volumecontroller.spec.ts └── vttutils.spec.ts ├── src ├── html │ ├── index.html │ └── simple.html ├── scss │ ├── bitmovinplayer-ui.scss │ ├── demo.scss │ └── skin-modern │ │ ├── _mixins.scss │ │ ├── _skin-ads.scss │ │ ├── _skin-cast-receiver.scss │ │ ├── _skin-smallscreen.scss │ │ ├── _skin-tv.scss │ │ ├── _skin.scss │ │ ├── _variables.scss │ │ └── components │ │ ├── _airplaytogglebutton.scss │ │ ├── _audiotracksettingspaneltogglebutton.scss │ │ ├── _bufferingoverlay.scss │ │ ├── _button.scss │ │ ├── _caststatusoverlay.scss │ │ ├── _casttogglebutton.scss │ │ ├── _clickoverlay.scss │ │ ├── _closebutton.scss │ │ ├── _component.scss │ │ ├── _container.scss │ │ ├── _controlbar.scss │ │ ├── _ecomodetogglebutton.scss │ │ ├── _errormessageoverlay.scss │ │ ├── _fullscreentogglebutton.scss │ │ ├── _hugeplaybacktogglebutton.scss │ │ ├── _hugereplaybutton.scss │ │ ├── _label.scss │ │ ├── _listbox.scss │ │ ├── _pictureinpicturetogglebutton.scss │ │ ├── _playbacktimelabel.scss │ │ ├── _playbacktogglebutton.scss │ │ ├── _playbacktoggleoverlay.scss │ │ ├── _quickseekbutton.scss │ │ ├── _recommendationoverlay.scss │ │ ├── _replaybutton.scss │ │ ├── _seekbar.scss │ │ ├── _seekbarlabel.scss │ │ ├── _selectbox.scss │ │ ├── _settingspanel.scss │ │ ├── _settingspanelpage.scss │ │ ├── _settingspanelpagebackbutton.scss │ │ ├── _settingspanelpageopenbutton.scss │ │ ├── _settingstogglebutton.scss │ │ ├── _spacer.scss │ │ ├── _subtitleoverlay-cea608.scss │ │ ├── _subtitleoverlay.scss │ │ ├── _subtitlesettingspaneltogglebutton.scss │ │ ├── _titlebar.scss │ │ ├── _uicontainer.scss │ │ ├── _volumecontrolbutton.scss │ │ ├── _volumeslider.scss │ │ ├── _volumetogglebutton.scss │ │ ├── _vrtogglebutton.scss │ │ ├── _watermark.scss │ │ └── subtitlesettings │ │ ├── _subtitleoverlay-settings.scss │ │ ├── _subtitlesettings.scss │ │ └── _subtitlesettingsresetbutton.scss └── ts │ ├── arrayutils.ts │ ├── audiotrackutils.ts │ ├── browserutils.ts │ ├── components │ ├── adclickoverlay.ts │ ├── admessagelabel.ts │ ├── adskipbutton.ts │ ├── airplaytogglebutton.ts │ ├── audioqualityselectbox.ts │ ├── audiotracklistbox.ts │ ├── audiotrackselectbox.ts │ ├── bufferingoverlay.ts │ ├── button.ts │ ├── caststatusoverlay.ts │ ├── casttogglebutton.ts │ ├── castuicontainer.ts │ ├── clickoverlay.ts │ ├── closebutton.ts │ ├── component.ts │ ├── container.ts │ ├── controlbar.ts │ ├── ecomodecontainer.ts │ ├── ecomodetogglebutton.ts │ ├── errormessageoverlay.ts │ ├── fullscreentogglebutton.ts │ ├── hugeplaybacktogglebutton.ts │ ├── hugereplaybutton.ts │ ├── itemselectionlist.ts │ ├── label.ts │ ├── listbox.ts │ ├── listselector.ts │ ├── metadatalabel.ts │ ├── pictureinpicturetogglebutton.ts │ ├── playbackspeedselectbox.ts │ ├── playbacktimelabel.ts │ ├── playbacktogglebutton.ts │ ├── playbacktoggleoverlay.ts │ ├── quickseekbutton.ts │ ├── recommendationoverlay.ts │ ├── replaybutton.ts │ ├── seekbar.ts │ ├── seekbarbufferlevel.ts │ ├── seekbarcontroller.ts │ ├── seekbarlabel.ts │ ├── selectbox.ts │ ├── settingspanel.ts │ ├── settingspanelitem.ts │ ├── settingspanelpage.ts │ ├── settingspanelpagebackbutton.ts │ ├── settingspanelpagenavigatorbutton.ts │ ├── settingspanelpageopenbutton.ts │ ├── settingstogglebutton.ts │ ├── spacer.ts │ ├── subtitlelistbox.ts │ ├── subtitleoverlay.ts │ ├── subtitleselectbox.ts │ ├── subtitlesettings │ │ ├── backgroundcolorselectbox.ts │ │ ├── backgroundopacityselectbox.ts │ │ ├── characteredgecolorselectbox.ts │ │ ├── characteredgeselectbox.ts │ │ ├── fontcolorselectbox.ts │ │ ├── fontfamilyselectbox.ts │ │ ├── fontopacityselectbox.ts │ │ ├── fontsizeselectbox.ts │ │ ├── fontstyleselectbox.ts │ │ ├── subtitlesettingselectbox.ts │ │ ├── subtitlesettingslabel.ts │ │ ├── subtitlesettingsmanager.ts │ │ ├── subtitlesettingspanelpage.ts │ │ ├── subtitlesettingsresetbutton.ts │ │ ├── windowcolorselectbox.ts │ │ └── windowopacityselectbox.ts │ ├── timelinemarkershandler.ts │ ├── titlebar.ts │ ├── togglebutton.ts │ ├── tvnoisecanvas.ts │ ├── uicontainer.ts │ ├── videoqualityselectbox.ts │ ├── volumecontrolbutton.ts │ ├── volumeslider.ts │ ├── volumetogglebutton.ts │ ├── vrtogglebutton.ts │ └── watermark.ts │ ├── demofactory.ts │ ├── dom.ts │ ├── errorutils.ts │ ├── eventdispatcher.ts │ ├── focusvisibilitytracker.ts │ ├── groupplaybackapi.ts │ ├── guid.ts │ ├── imageloader.ts │ ├── localization │ ├── i18n.ts │ └── languages │ │ ├── de.json │ │ ├── en.json │ │ ├── es.json │ │ └── nl.json │ ├── main.ts │ ├── mobilev3playerapi.ts │ ├── playerutils.ts │ ├── spatialnavigation │ ├── ListNavigationGroup.ts │ ├── gethtmlelementsfromcomponents.ts │ ├── keymap.ts │ ├── navigationalgorithm.ts │ ├── navigationgroup.ts │ ├── nodeeventsubscriber.ts │ ├── rootnavigationgroup.ts │ ├── seekbarhandler.ts │ ├── spatialnavigation.ts │ ├── typeguards.ts │ └── types.ts │ ├── storageutils.ts │ ├── stringutils.ts │ ├── subtitleutils.ts │ ├── timeout.ts │ ├── uiconfig.ts │ ├── uifactory.ts │ ├── uimanager.ts │ ├── uiutils.ts │ ├── volumecontroller.ts │ └── vttutils.ts ├── tsconfig.json └── typedoc.json /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Report a Bug 4 | url: https://dashboard.bitmovin.com/support/tickets 5 | about: Report a Bug you encountered in our Bitmovin Player UI in your Bitmovin Customer Dashboard. 6 | - name: Feature Requests 7 | url: https://community.bitmovin.com/t/how-to-submit-a-feature-request-to-us/1463 8 | about: Learn how to suggest new features for our Player SDKs. 9 | - name: Report a security vulnerability 10 | url: https://bitmovin.atlassian.net/wiki/external/1502085332/YTYwODMwZjQyNjkwNGQ0ODg5MTgwM2NhMDliNjRmODE 11 | about: Report a security vulnerability. -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | 4 | ## Checklist (for PR submitter and reviewers) 5 | 6 | 7 | - [ ] `CHANGELOG` entry 8 | -------------------------------------------------------------------------------- /.github/scripts/defineVersion.js: -------------------------------------------------------------------------------- 1 | const semver = require('semver'); 2 | 3 | function getPlayerUiVersion(versionInput) { 4 | const playerUiVersion = semver.valid(versionInput); 5 | if (!playerUiVersion) { 6 | console.error(`${versionInput} is not a valid semver`); 7 | process.exit(1); 8 | } 9 | 10 | return { 11 | major: semver.major(playerUiVersion), 12 | minor: semver.minor(playerUiVersion), 13 | patch: semver.patch(playerUiVersion), 14 | prereleaseLabels: semver.prerelease(playerUiVersion), 15 | full: playerUiVersion, 16 | }; 17 | } 18 | 19 | function defineReleaseVersion({ core }, targetReleaseLevel, givenVersion) { 20 | core.info(`Defining new release version for level ${targetReleaseLevel} and version ${givenVersion}`); 21 | 22 | const newVersion = semver.inc(givenVersion, targetReleaseLevel); 23 | 24 | const parsedPlayerVersion = getPlayerUiVersion(newVersion); 25 | core.info(`Using release version ${parsedPlayerVersion.full}`); 26 | return parsedPlayerVersion; 27 | } 28 | 29 | module.exports.defineReleaseVersion = defineReleaseVersion; 30 | -------------------------------------------------------------------------------- /.github/scripts/notifySlackTeam.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const https = require('https'); 3 | 4 | const jobStatus = process.argv[2]; 5 | const changelogPath = process.argv[3]; 6 | const slackWebhookUrl = process.argv[4]; 7 | const runId = process.argv[5]; 8 | 9 | const failureSlackChannelId = 'CGRK9DV7H'; 10 | const successSlackChannelId = 'C0LJ16JBS'; 11 | 12 | fs.readFile(changelogPath, 'utf8', (err, fileContent) => { 13 | if (err) { 14 | throw err; 15 | } 16 | 17 | const changelogContent = parseChangelogEntry(fileContent); 18 | const releaseVersion = parseReleaseVersion(fileContent); 19 | sendSlackMessage(releaseVersion, changelogContent); 20 | }); 21 | 22 | function parseReleaseVersion(fileContent) { 23 | const regex = /##\s\[(\d+\.\d+.\d+)\]/; 24 | const releaseVersion = fileContent.match(regex); 25 | 26 | return releaseVersion[1]; 27 | } 28 | 29 | function parseChangelogEntry(fileContent) { 30 | // The regex looks for the first paragraph starting with "###" until it finds 31 | // a paragraph starting with "##". 32 | // For some reason it also matches 2 chars at the end. With the .slice 33 | // those 2 chars get removed from the string. 34 | const regex = /###(.)*[\s\S]*?(?=\s##\s\[v*?)/; 35 | 36 | let changelogContent = fileContent.match(regex); 37 | changelogContent = changelogContent.slice(0, -1); 38 | return changelogContent.toString(); 39 | } 40 | 41 | function sendSlackMessage(releaseVersion, changelogContent) { 42 | let message; 43 | let slackChannelId; 44 | if (jobStatus === 'success') { 45 | slackChannelId = successSlackChannelId 46 | message = `Changelog v${releaseVersion}\n${changelogContent}` 47 | } else { 48 | slackChannelId = failureSlackChannelId 49 | message = `Release v${releaseVersion} failed.\nPlease check https://github.com/bitmovin/bitmovin-player-ui/actions/runs/${runId}` 50 | } 51 | 52 | const sampleData = JSON.stringify({ 53 | "channel": slackChannelId, 54 | "message": message 55 | }); 56 | const options = { 57 | method: 'POST', 58 | headers: { 59 | 'Content-Type': 'application/json', 60 | 'Accept': "application/json", 61 | } 62 | }; 63 | 64 | var req = https.request(slackWebhookUrl, options, (res) => { 65 | console.log('statusCode:', res.statusCode); 66 | console.log('headers:', res.headers); 67 | 68 | res.on('data', (d) => { 69 | process.stdout.write(d); 70 | }); 71 | }); 72 | 73 | req.on('error', (e) => { 74 | console.error(e); 75 | }); 76 | 77 | req.write(sampleData); 78 | req.end(); 79 | } 80 | -------------------------------------------------------------------------------- /.github/scripts/updateChangelog.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Updates the changelog by replacing the changelog header with the correct version 3 | * 4 | * @param {string} changelogString the content of the changelog file 5 | * @param {string} version the player version to be released 6 | * @param {string} releaseDate the release date to be written to the changelog 7 | */ 8 | function updateChangeLog(changelogString, version, releaseDate) { 9 | const optionalBetaOrRc = '(-rc.d+)?(-(b|beta).d+)?'; 10 | const changelogVersionRegExp = new RegExp( 11 | `\\[(development|develop|unreleased|${version})${optionalBetaOrRc}.*`, 12 | 'gi', 13 | ); 14 | 15 | const lastReleaseVersion = changelogString.match(/## \[(\d+.\d+.\d+)\] - \d{4}-\d{2}-\d{2}/)[1]; 16 | const changelogWithReleaseVersionAndDate = changelogString.replace(changelogVersionRegExp, `[${version}] - ${releaseDate}`); 17 | 18 | return changelogWithReleaseVersionAndDate.replace( 19 | '## 1.0.0 (2017-02-03)\n- First release\n\n', 20 | `## 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` 21 | ); 22 | } 23 | 24 | module.exports.updateChangeLog = updateChangeLog; 25 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | 8 | workflow_dispatch: 9 | 10 | workflow_call: 11 | 12 | jobs: 13 | test_and_build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | 21 | - name: Set up node.js 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version-file: .nvmrc 25 | 26 | - name: Cache node modules 27 | id: cache-nodemodules 28 | uses: actions/cache@v4 29 | env: 30 | cache-name: cache-node-modules 31 | with: 32 | path: node_modules 33 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 34 | 35 | - name: Install dependencies 36 | if: steps.cache-nodemodules.outputs.cache-hit != 'true' 37 | run: npm ci 38 | 39 | - name: Test 40 | run: npm test 41 | 42 | - name: Build and prepare for a potential 'npm publish' 43 | run: gulp npm-prepare 44 | 45 | - name: Package artifact for upload 46 | run: tar -czvf artifact.tar.gz dist 47 | shell: bash 48 | 49 | - uses: actions/upload-artifact@v4 50 | with: 51 | path: | 52 | ${{ github.workspace }}/artifact.tar.gz 53 | if-no-files-found: error 54 | retention-days: 1 55 | 56 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | lint-staged 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v22.12 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Bitmovin Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /assets/skin-modern/images/airplay.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /assets/skin-modern/images/airplayX.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /assets/skin-modern/images/audio-tracks.svg: -------------------------------------------------------------------------------- 1 | Artboard 1 -------------------------------------------------------------------------------- /assets/skin-modern/images/audio-tracksX.svg: -------------------------------------------------------------------------------- 1 | Artboard 1 -------------------------------------------------------------------------------- /assets/skin-modern/images/chromecast.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /assets/skin-modern/images/chromecastX.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /assets/skin-modern/images/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /assets/skin-modern/images/closeX.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /assets/skin-modern/images/dot-play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /assets/skin-modern/images/dot-volume.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /assets/skin-modern/images/fullscreen.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /assets/skin-modern/images/fullscreenX.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /assets/skin-modern/images/glasses.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 20 | 21 | 22 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /assets/skin-modern/images/glassesX.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /assets/skin-modern/images/leaf.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/skin-modern/images/loader.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /assets/skin-modern/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /assets/skin-modern/images/music-low.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /assets/skin-modern/images/music-lowX.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /assets/skin-modern/images/music-off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 12 | 13 | 14 | 15 | 16 | 17 | 19 | 20 | 21 | 22 | 23 | 25 | 26 | 27 | 28 | 29 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /assets/skin-modern/images/music-offX.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | 21 | 22 | 23 | 24 | 26 | 27 | 28 | 29 | 30 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /assets/skin-modern/images/music-on.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /assets/skin-modern/images/music-onX.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /assets/skin-modern/images/next.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /assets/skin-modern/images/nextX.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /assets/skin-modern/images/pause.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /assets/skin-modern/images/pauseX.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /assets/skin-modern/images/picinpic1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /assets/skin-modern/images/picinpic1X.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /assets/skin-modern/images/picinpic2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /assets/skin-modern/images/picinpic2X.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /assets/skin-modern/images/play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /assets/skin-modern/images/playX.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /assets/skin-modern/images/play_big.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 11 | 12 | 13 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /assets/skin-modern/images/play_bigX.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 12 | 13 | 14 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /assets/skin-modern/images/playlist.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /assets/skin-modern/images/playlistX.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /assets/skin-modern/images/prev.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /assets/skin-modern/images/prevX.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /assets/skin-modern/images/quickseek-fastforward.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /assets/skin-modern/images/quickseek-rewind.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/skin-modern/images/replay-nocircle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /assets/skin-modern/images/replay.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /assets/skin-modern/images/replayX.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /assets/skin-modern/images/stop.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /assets/skin-modern/images/stopX.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /assets/skin-modern/images/subtitles.svg: -------------------------------------------------------------------------------- 1 | Artboard 1 -------------------------------------------------------------------------------- /assets/skin-modern/images/subtitlesX.svg: -------------------------------------------------------------------------------- 1 | Artboard 1 -------------------------------------------------------------------------------- /assets/skin-modern/images/toggleOff.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /assets/skin-modern/images/toggleOn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { dirname } from 'node:path'; 4 | import { fileURLToPath } from 'node:url'; 5 | import eslint from '@eslint/js'; 6 | import tseslint from 'typescript-eslint'; 7 | 8 | const __dirname = dirname(fileURLToPath(import.meta.url)); 9 | 10 | export default tseslint.config({ 11 | files: [ 12 | 'src/**/*.ts', 13 | 'spec/**/*.ts', 14 | ], 15 | extends: [ 16 | eslint.configs.recommended, 17 | ...tseslint.configs.recommendedTypeChecked, 18 | { 19 | languageOptions: { 20 | parserOptions: { 21 | projectService: true, 22 | tsconfigRootDir: __dirname, 23 | }, 24 | }, 25 | }, 26 | ], 27 | rules: { 28 | 'no-prototype-builtins': 'off', 29 | 'prefer-const': 'off', // TODO: enable rule and run autofix 30 | 'prefer-rest-params': 'off', 31 | '@typescript-eslint/ban-ts-comment': 'off', 32 | '@typescript-eslint/no-explicit-any': 'off', 33 | '@typescript-eslint/no-namespace': 'off', 34 | '@typescript-eslint/no-empty-object-type': 'off', 35 | '@typescript-eslint/no-wrapper-object-types': 'off', 36 | '@typescript-eslint/no-unused-vars': 'off', 37 | '@typescript-eslint/no-unnecessary-type-assertion': 'off', 38 | '@typescript-eslint/no-unsafe-enum-comparison': 'off', 39 | '@typescript-eslint/no-unsafe-member-access': 'off', 40 | '@typescript-eslint/no-unsafe-return': 'off', 41 | '@typescript-eslint/no-unsafe-assignment': 'off', 42 | '@typescript-eslint/no-unsafe-argument': 'off', 43 | '@typescript-eslint/no-redundant-type-constituents': 'off', 44 | '@typescript-eslint/no-floating-promises': 'off', 45 | '@typescript-eslint/no-unused-expressions': ['error', { 46 | allowTernary: true, 47 | }] 48 | }, 49 | }); 50 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | 6 | // Automatically clear mock calls and instances between every test 7 | clearMocks: true, 8 | 9 | // The directory where Jest should output its coverage files 10 | coverageDirectory: "coverage", 11 | 12 | // The test environment that will be used for testing 13 | testEnvironment: "jsdom", 14 | 15 | // A map from regular expressions to paths to transformers 16 | transform: { 17 | '^.+\\.tsx?$': 'ts-jest' 18 | }, 19 | 20 | setupFilesAfterEnv: ['/setup-jest.ts'], 21 | }; 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bitmovin-player-ui", 3 | "version": "3.96.0", 4 | "description": "Bitmovin Player UI Framework", 5 | "main": "./dist/js/framework/main.js", 6 | "types": "./dist/js/framework/main.d.ts", 7 | "scripts": { 8 | "test": "npx jest", 9 | "prepare": "husky" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/bitmovin/bitmovin-player-ui.git" 14 | }, 15 | "author": "Bitmovin", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/bitmovin/bitmovin-player-ui/issues" 19 | }, 20 | "homepage": "https://github.com/bitmovin/bitmovin-player-ui#readme", 21 | "browserslist": [ 22 | "> 1%", 23 | "last 2 versions", 24 | "Firefox ESR" 25 | ], 26 | "lint-staged": { 27 | "*.ts": [ 28 | "eslint --fix" 29 | ] 30 | }, 31 | "devDependencies": { 32 | "@eslint/js": "^9.17.0", 33 | "@inrupt/jest-jsdom-polyfills": "^1.6.0", 34 | "@types/jest": "^29.5.0", 35 | "@types/jsdom": "^21.1.0", 36 | "autoprefixer": "^10.4.14", 37 | "bitmovin-player": "^8.129.0", 38 | "browser-sync": "^2.29.0", 39 | "browserify": "^17.0.0", 40 | "cssnano": "^5.1.15", 41 | "del": "^2.2.2", 42 | "eslint": "^9.17.0", 43 | "gulp": "^4.0.2", 44 | "gulp-css-base64": "^2.0.0", 45 | "gulp-eslint-new": "^2.4.0", 46 | "gulp-header": "^2.0.9", 47 | "gulp-postcss": "^9.0.1", 48 | "gulp-rename": "^2.0.0", 49 | "gulp-replace": "^1.1.4", 50 | "gulp-sass": "^5.1.0", 51 | "gulp-sass-lint": "^1.4.0", 52 | "gulp-sourcemaps": "^3.0.0", 53 | "gulp-typescript": "^5.0.1", 54 | "gulp-uglify": "^3.0.2", 55 | "husky": "^9.1.7", 56 | "jest": "^29.5.0", 57 | "jest-environment-jsdom": "^29.5.0", 58 | "lint-staged": "^15.3.0", 59 | "merge2": "^1.4.1", 60 | "postcss-svg": "^3.0.0", 61 | "sass": "^1.59.3", 62 | "semver": "^7.5.4", 63 | "stream-combiner2": "^1.1.1", 64 | "ts-jest": "^29.0.5", 65 | "tsify": "^5.0.4", 66 | "typedoc": "^0.26.5", 67 | "typescript": "^5.0.2", 68 | "typescript-eslint": "^8.18.1", 69 | "vinyl-buffer": "^1.0.1", 70 | "vinyl-source-stream": "^2.0.0", 71 | "watchify": "^4.0.0", 72 | "yargs": "^17.7.1" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /setup-jest.ts: -------------------------------------------------------------------------------- 1 | import '@inrupt/jest-jsdom-polyfills'; 2 | -------------------------------------------------------------------------------- /spec/browserutils.spec.ts: -------------------------------------------------------------------------------- 1 | import { BrowserUtils } from '../src/ts/browserutils'; 2 | 3 | const mobileSafariUserAgent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A356 Safari/604.1'; 4 | 5 | declare const global: any; 6 | 7 | describe('BrowserUtils', () => { 8 | beforeEach(() => { 9 | Object.defineProperty(global.navigator, 'userAgent', { 10 | get() { 11 | return mobileSafariUserAgent; 12 | }, 13 | configurable: true, 14 | }); 15 | }); 16 | 17 | afterEach(() => { 18 | delete global.navigator.userAgent; 19 | }); 20 | 21 | describe('isMobile', () => { 22 | it('detects mobile if user agent contains Mobi', () => { 23 | expect(BrowserUtils.isMobile).toBeTruthy(); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /spec/errorutils.spec.ts: -------------------------------------------------------------------------------- 1 | import { MobileV3PlayerErrorEvent } from '../src/ts/mobilev3playerapi'; 2 | import { ErrorUtils } from '../src/ts/errorutils'; 3 | import defaultMobileV3ErrorMessageTranslator = ErrorUtils.defaultMobileV3ErrorMessageTranslator; 4 | import { ErrorEvent } from 'bitmovin-player'; 5 | import defaultWebErrorMessageTranslator = ErrorUtils.defaultWebErrorMessageTranslator; 6 | 7 | describe('ErrorUtils', () => { 8 | describe('defaultMobileV3ErrorMessageTranslator', () => { 9 | it('translates the error event to an error message', () => { 10 | const errorMessage = 'something went horribly wrong'; 11 | const playerErrorEvent = { message: errorMessage } as MobileV3PlayerErrorEvent; 12 | 13 | expect(defaultMobileV3ErrorMessageTranslator(playerErrorEvent)).toEqual(errorMessage); 14 | }); 15 | }); 16 | 17 | describe('defaultWebErrorMessageTranslator', () => { 18 | it('maps the error code to the error message and return it', () => { 19 | const errorCode = 2100; 20 | const errorName = 'player-error'; 21 | const errorEvent = { code: errorCode, name: errorName } as ErrorEvent; 22 | const expectedErrorMessage = ErrorUtils.defaultErrorMessages[errorCode]; 23 | 24 | expect(defaultWebErrorMessageTranslator(errorEvent)).toEqual(`${expectedErrorMessage}\n(${errorName})`); 25 | }); 26 | 27 | it('falls back to returning the error code and name if the associated error message could not be found', () => { 28 | const errorCode = 9999; 29 | const errorName = 'unknown-error'; 30 | const errorEvent = { code: errorCode as unknown, name: errorName } as ErrorEvent; 31 | 32 | expect(defaultWebErrorMessageTranslator(errorEvent)).toEqual(`${errorCode} ${errorName}`); 33 | }); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /spec/helper/mockClass.ts: -------------------------------------------------------------------------------- 1 | type Constructor = { new (...params: any[]): T }; 2 | type GenericMock = { [key: string]: jest.Mock }; 3 | 4 | function isStringIndexable(obj: object): obj is { [key: string]: unknown } { 5 | return Boolean(obj) && Object.getOwnPropertyNames(obj).length > 0; 6 | } 7 | 8 | function isFunction(obj: object, key: string): boolean { 9 | try { 10 | return isStringIndexable(obj) && obj[key] instanceof Function; 11 | } catch { 12 | return false; 13 | } 14 | } 15 | 16 | function resolveClassMethods(obj?: Constructor, classMethods: string[] = []): string[] { 17 | if (!obj) { 18 | return classMethods; 19 | } 20 | 21 | const newClassMethods = [...classMethods, ...Object.getOwnPropertyNames(obj).filter(name => isFunction(obj, name))]; 22 | 23 | return resolveClassMethods(Object.getPrototypeOf(obj), newClassMethods); 24 | } 25 | 26 | function addMockMethod(mock: GenericMock, methodName: string): GenericMock { 27 | mock[methodName] = jest.fn(); 28 | 29 | return mock; 30 | } 31 | 32 | export function getClassMethods(constructor: Constructor) { 33 | return resolveClassMethods(constructor); 34 | } 35 | 36 | export function mockClass(constructor: Constructor): jest.Mocked { 37 | return getClassMethods(constructor.prototype).reduce( 38 | addMockMethod, 39 | jest.fn() as unknown as GenericMock, 40 | ) as unknown as jest.Mocked; 41 | } 42 | 43 | export function mockObject(propertyNames: string[]): object { 44 | return propertyNames.reduce((obj, name) => { 45 | (obj as any)[name] = jest.fn(); 46 | return obj; 47 | }, {}); 48 | } 49 | -------------------------------------------------------------------------------- /spec/helper/mockComponent.ts: -------------------------------------------------------------------------------- 1 | import { mockClass, mockObject } from './mockClass'; 2 | import { MockHelper } from './MockHelper'; 3 | 4 | type ConstructorType = new (...args : any[]) => T; 5 | 6 | export function mockHtmlElement() { 7 | return mockObject(['focus', 'blur', 'addEventListener', 'removeEventListener', 'children', 'click']) as jest.Mocked; 8 | } 9 | 10 | export function mockComponent>(component: T) { 11 | const componentMock: jest.Mocked> = mockClass(component); 12 | const componentDOMMock = MockHelper.generateDOMMock(); 13 | const componentHTMLMock = mockHtmlElement(); 14 | 15 | componentDOMMock.get.mockReturnValue([componentHTMLMock] as any); 16 | 17 | componentMock.getDomElement.mockReturnValue(componentDOMMock); 18 | 19 | return componentMock; 20 | } 21 | 22 | export function getFirstDomElement(componentMock: jest.Mocked): jest.Mocked { 23 | return componentMock.getDomElement().get()[0] as jest.Mocked; 24 | } 25 | -------------------------------------------------------------------------------- /spec/localization.spec.ts: -------------------------------------------------------------------------------- 1 | import { i18n } from '../src/ts/localization/i18n'; 2 | 3 | const fallbackTest = 'fallback test'; 4 | const successEn = 'success'; 5 | const successDe = 'erfolg'; 6 | const successIt = 'successo'; 7 | 8 | const defaultConfig = { 9 | language: 'en', 10 | vocabularies: { 11 | 'it': { 12 | 'test': successIt, 13 | [fallbackTest]: successIt, 14 | }, 15 | 'en': { 16 | 'test': successEn, 17 | [fallbackTest]: successEn, 18 | 'variableTest': `{value}`, 19 | }, 20 | 'de': { 21 | 'test': successDe, 22 | }, 23 | }, 24 | }; 25 | 26 | describe('Localization', () => { 27 | beforeEach(() => { 28 | i18n.setConfig(defaultConfig); 29 | }); 30 | 31 | describe('Locale initialiization', () => { 32 | it('uses vocabulary \'en\'', () => { 33 | expect(i18n.performLocalization(i18n.getLocalizer('test'))).toEqual(successEn); 34 | }); 35 | 36 | it('uses vocabulary \'de\'', () => { 37 | i18n.setConfig({ ...defaultConfig, language: 'de' }); 38 | expect(i18n.performLocalization(i18n.getLocalizer('test'))).toEqual(successDe); 39 | }); 40 | 41 | it('uses vocabulary \'it\'', () => { 42 | i18n.setConfig({ ...defaultConfig, language: 'it' }); 43 | expect(i18n.performLocalization(i18n.getLocalizer('test'))).toEqual(successIt); 44 | }); 45 | }); 46 | 47 | describe('Language Fallback\'s', () => { 48 | it('falls back to `key` if it is not in vocabulary', () => { 49 | expect(i18n.performLocalization(i18n.getLocalizer('some word'))).toEqual('some word'); 50 | }); 51 | 52 | it('falls back to', () => { 53 | i18n.setConfig({ ...defaultConfig, language: 'de' }); 54 | expect(i18n.performLocalization(i18n.getLocalizer(fallbackTest))).toEqual(successEn); 55 | }); 56 | }); 57 | 58 | describe('Variable Injection', () => { 59 | it ('injects the value to string passed by config', () => { 60 | expect(i18n.performLocalization(i18n.getLocalizer('variableTest', { value: 1 }))).toEqual('1'); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /spec/mobilev3playerapi.spec.ts: -------------------------------------------------------------------------------- 1 | import { PlayerAPI } from 'bitmovin-player'; 2 | import { isMobileV3PlayerAPI, MobileV3PlayerAPI, MobileV3PlayerEvent } from '../src/ts/mobilev3playerapi'; 3 | import { PlayerWrapper } from '../src/ts/uimanager'; 4 | 5 | describe('isMobileV3PlayerAPI', () => { 6 | const playerApi = { exports: { PlayerEvent: { } } } as PlayerAPI; 7 | const mobileV3PlayerApi = { exports: { PlayerEvent: MobileV3PlayerEvent } } as unknown as MobileV3PlayerAPI; 8 | const wrappedPlayerApi = new PlayerWrapper(playerApi); 9 | const wrappedMobileV3PlayerApi = new PlayerWrapper(mobileV3PlayerApi); 10 | 11 | it('should return false for a regular PlayerAPI instance', () => { 12 | expect(isMobileV3PlayerAPI(playerApi)).toBeFalsy(); 13 | }); 14 | 15 | it('should return false for a regular wrapped PlayerAPI instance', () => { 16 | expect(isMobileV3PlayerAPI(wrappedPlayerApi.getPlayer())).toBeFalsy(); 17 | }); 18 | 19 | it('should return true for a mobile v3 PlayerAPI instance', () => { 20 | expect(isMobileV3PlayerAPI(mobileV3PlayerApi)).toBeTruthy(); 21 | }); 22 | 23 | it('should return false for a mobile v3 wrapped PlayerAPI instance', () => { 24 | expect(isMobileV3PlayerAPI(wrappedMobileV3PlayerApi.getPlayer())).toBeTruthy(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /spec/playerutils.spec.ts: -------------------------------------------------------------------------------- 1 | import { PlayerUtils } from '../src/ts/playerutils'; 2 | import { PlayerAPI } from 'bitmovin-player'; 3 | import { MockHelper } from './helper/MockHelper'; 4 | 5 | describe('PlayerUtils', () => { 6 | let playerMock: PlayerAPI; 7 | 8 | beforeEach(() => { 9 | playerMock = MockHelper.getPlayerMock(); 10 | }); 11 | 12 | describe('getSeekableRange', () => { 13 | it('it should return seekable range from playerAPI if not live', () => { 14 | const seekableRange = { start: 0, end: 15 }; 15 | jest.spyOn(playerMock, 'isLive').mockReturnValue(false); 16 | jest.spyOn(playerMock, 'getSeekableRange').mockReturnValue(seekableRange); 17 | 18 | const utilSeekableRange = PlayerUtils.getSeekableRangeRespectingLive(playerMock); 19 | 20 | expect(utilSeekableRange).toEqual(seekableRange); 21 | }); 22 | 23 | test.each` 24 | timeshift | maxTimeshift | currentTime | expectedStart | expectedEnd 25 | ${0} | ${-260} | ${1594646367} | ${1594646107} | ${1594646367} 26 | ${-25} | ${-260} | ${1594646367} | ${1594646132} | ${1594646392} 27 | ${-60} | ${-260} | ${1594646367} | ${1594646167} | ${1594646427} 28 | `( 29 | 'should calculate start=$expectedStart and end=$expectedEnd seekable range', 30 | ({ timeshift, maxTimeshift, currentTime, expectedStart, expectedEnd }) => { 31 | jest.spyOn(playerMock, 'isLive').mockReturnValue(true); 32 | jest.spyOn(playerMock, 'getTimeShift').mockReturnValue(timeshift); 33 | jest.spyOn(playerMock, 'getMaxTimeShift').mockReturnValue(maxTimeshift); 34 | jest.spyOn(playerMock, 'getCurrentTime').mockReturnValue(currentTime); 35 | 36 | const { start, end } = PlayerUtils.getSeekableRangeRespectingLive(playerMock); 37 | 38 | expect(start).toEqual(expectedStart); 39 | expect(end).toEqual(expectedEnd); 40 | }, 41 | ); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /spec/release/defineVersion.spec.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-require-imports 2 | const { defineReleaseVersion } = require('../../.github/scripts/defineVersion'); 3 | 4 | describe('defineReleaseVersion', () => { 5 | test.each` 6 | existingVersion | desiredReleaseLevel | expectedVersion 7 | ${'1.0.0'} | ${'major'} | ${'2.0.0'} 8 | ${'1.0.0'} | ${'minor'} | ${'1.1.0'} 9 | ${'1.1.0'} | ${'minor'} | ${'1.2.0'} 10 | ${'1.0.0'} | ${'patch'} | ${'1.0.1'} 11 | ${'1.1.0'} | ${'patch'} | ${'1.1.1'} 12 | ${'1.0.4'} | ${'patch'} | ${'1.0.5'} 13 | ${'1.0.0'} | ${'minor'} | ${'1.1.0'} 14 | ${'1.0.0'} | ${'minor'} | ${'1.1.0'} 15 | ${'1.0.0'} | ${'major'} | ${'2.0.0'} 16 | ${'1.5.0'} | ${'major'} | ${'2.0.0'} 17 | ${'2.0.0'} | ${'major'} | ${'3.0.0'} 18 | `( 19 | 'should return version $expectedVersion with version $existingVersion and $desiredReleaseLevel release level', 20 | ({ existingVersion, desiredReleaseLevel, expectedVersion }) => { 21 | const core = { info() {} }; 22 | const result = defineReleaseVersion({ core }, desiredReleaseLevel, existingVersion); 23 | 24 | expect(result.full).toEqual(expectedVersion); 25 | }, 26 | ); 27 | }); 28 | -------------------------------------------------------------------------------- /spec/spatialnavigation/keymap.spec.ts: -------------------------------------------------------------------------------- 1 | import { getKeyMapForPlatform } from '../../src/ts/spatialnavigation/keymap'; 2 | import { Action } from '../../src/ts/spatialnavigation/types'; 3 | 4 | const userAgent = { 5 | chrome: 6 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36', 7 | tizen: 8 | 'Mozilla/5.0 (SMART-TV; LINUX; Tizen 4.0) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 TV Safari/537.36', 9 | webOs: 10 | 'Mozilla/5.0 (Web0S; Linux/SmartTV) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.79 Safari/537.36 WebAppManager', 11 | playStation5: 12 | 'Mozilla/5.0 (PlayStation; PlayStation 5/5.02) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Safari/605.1.15', 13 | android: 14 | 'Mozilla/5.0 (Linux; U; Android 2.2.1; en-us; Nexus One Build/FRG83) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1', 15 | hisense: 16 | 'Mozilla/5.0 (Linux; Android 7.0; Hisense F102) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.67 Mobile Safari/537.36', 17 | }; 18 | 19 | describe('getKeyMapForPlatform', () => { 20 | test.each` 21 | userAgent | expectedBackKey 22 | ${userAgent.tizen} | ${10009} 23 | ${userAgent.webOs} | ${461} 24 | ${userAgent.chrome} | ${27} 25 | ${userAgent.playStation5} | ${27} 26 | ${userAgent.hisense} | ${8} 27 | ${userAgent.android} | ${27} 28 | `('should return a key map with [$expectedBackKey]=Actions.BACK', ({ userAgent, expectedBackKey }) => { 29 | const userAgentSpy = jest.spyOn(window.navigator, 'userAgent', 'get').mockReturnValue(userAgent); 30 | const keyMap = getKeyMapForPlatform(); 31 | 32 | expect(keyMap[expectedBackKey]).toEqual(Action.BACK); 33 | userAgentSpy.mockRestore(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /spec/spatialnavigation/listnavigationgroup.spec.ts: -------------------------------------------------------------------------------- 1 | import { UIContainer } from '../../src/ts/components/uicontainer'; 2 | import { mockClass } from '../helper/mockClass'; 3 | import { Action, Direction } from '../../src/ts/spatialnavigation/types'; 4 | import { ListNavigationGroup, ListOrientation } from '../../src/ts/spatialnavigation/ListNavigationGroup'; 5 | 6 | jest.mock('../../src/ts/spatialnavigation/navigationgroup.ts'); 7 | 8 | describe('ListNavigationGroup', () => { 9 | let containerUiMock: jest.Mocked; 10 | 11 | beforeEach(() => { 12 | containerUiMock = mockClass(UIContainer); 13 | containerUiMock.showUi = jest.fn(); 14 | containerUiMock.hideUi = jest.fn(); 15 | }); 16 | 17 | describe('handleAction', () => { 18 | it('should dispatch an Action.BACK after an Action.SELECT was tracked', () => { 19 | const listNavigationGroup = new ListNavigationGroup(ListOrientation.Vertical, containerUiMock); 20 | const handleActionSpy = jest.spyOn(listNavigationGroup, 'handleAction'); 21 | 22 | listNavigationGroup.handleAction(Action.SELECT); 23 | 24 | expect(handleActionSpy).toHaveBeenCalledWith(Action.BACK); 25 | }); 26 | }); 27 | 28 | describe('handleNavigation', () => { 29 | test.each` 30 | direction | orientation | shouldDispatch 31 | ${Direction.UP} | ${ListOrientation.Vertical} | ${false} 32 | ${Direction.DOWN} | ${ListOrientation.Vertical} | ${false} 33 | ${Direction.LEFT} | ${ListOrientation.Vertical} | ${true} 34 | ${Direction.RIGHT} | ${ListOrientation.Vertical} | ${true} 35 | ${Direction.UP} | ${ListOrientation.Horizontal} | ${true} 36 | ${Direction.DOWN} | ${ListOrientation.Horizontal} | ${true} 37 | ${Direction.LEFT} | ${ListOrientation.Horizontal} | ${false} 38 | ${Direction.RIGHT} | ${ListOrientation.Horizontal} | ${false} 39 | `( 40 | 'should dispatch an Action.BACK=$shouldDispatch on Direction.$direction with Orientation.$orientation', 41 | ({ direction, orientation, shouldDispatch }) => { 42 | const listNavigationGroup = new ListNavigationGroup(orientation, containerUiMock); 43 | const handleActionSpy = jest.spyOn(listNavigationGroup, 'handleAction'); 44 | 45 | listNavigationGroup.handleNavigation(direction); 46 | 47 | if (shouldDispatch) { 48 | expect(handleActionSpy).toHaveBeenCalledWith(Action.BACK); 49 | } else { 50 | expect(handleActionSpy).not.toHaveBeenCalled(); 51 | } 52 | }, 53 | ); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /spec/spatialnavigation/nodeeventsubscriber.spec.ts: -------------------------------------------------------------------------------- 1 | import { mockHtmlElement } from '../helper/mockComponent'; 2 | import { NodeEventSubscriber } from '../../src/ts/spatialnavigation/nodeeventsubscriber'; 3 | 4 | describe('NodeEventSubscriber', () => { 5 | let htmlElementMock: jest.Mocked; 6 | let nodeEventSubscriber: NodeEventSubscriber; 7 | 8 | beforeEach(() => { 9 | htmlElementMock = mockHtmlElement(); 10 | nodeEventSubscriber = new NodeEventSubscriber(); 11 | }); 12 | 13 | describe('on', () => { 14 | it('should attach event handler to passed in html element', () => { 15 | const listener = () => false; 16 | nodeEventSubscriber.on(htmlElementMock, 'click', listener); 17 | 18 | expect(htmlElementMock.addEventListener).toHaveBeenCalledWith('click', listener, undefined); 19 | }); 20 | }); 21 | 22 | describe('off', () => { 23 | it('should remove event handler from passed in html element', () => { 24 | const listener = () => false; 25 | nodeEventSubscriber.on(htmlElementMock, 'click', listener); 26 | nodeEventSubscriber.off(htmlElementMock, 'click', listener); 27 | 28 | expect(htmlElementMock.removeEventListener).toHaveBeenCalledWith('click', listener, undefined); 29 | }); 30 | }); 31 | 32 | describe('release', () => { 33 | it('should clear up attached listeners', () => { 34 | const listener = () => false; 35 | nodeEventSubscriber.on(htmlElementMock, 'click', listener); 36 | 37 | nodeEventSubscriber.release(); 38 | expect(htmlElementMock.removeEventListener).toHaveBeenCalledWith('click', listener, undefined); 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /spec/spatialnavigation/rootnavigationgroup.spec.ts: -------------------------------------------------------------------------------- 1 | import { UIContainer } from '../../src/ts/components/uicontainer'; 2 | import { mockClass } from '../helper/mockClass'; 3 | import { RootNavigationGroup } from '../../src/ts/spatialnavigation/rootnavigationgroup'; 4 | import { Action, Direction } from '../../src/ts/spatialnavigation/types'; 5 | 6 | jest.mock('../../src/ts/spatialnavigation/navigationgroup.ts'); 7 | 8 | describe('RootNavigationGroup', () => { 9 | let containerUiMock: jest.Mocked; 10 | let rootNavigationGroup: RootNavigationGroup; 11 | 12 | beforeEach(() => { 13 | containerUiMock = mockClass(UIContainer); 14 | containerUiMock.showUi = jest.fn(); 15 | containerUiMock.hideUi = jest.fn(); 16 | 17 | rootNavigationGroup = new RootNavigationGroup(containerUiMock); 18 | 19 | }); 20 | 21 | describe('handleAction', () => { 22 | it('should call showUi on UIContainer on Action.SELECT', () => { 23 | rootNavigationGroup.handleAction(Action.SELECT); 24 | 25 | expect(containerUiMock.showUi).toHaveBeenCalled(); 26 | }); 27 | 28 | it('should call hideUi on UIContainer on Action.BACK', () => { 29 | rootNavigationGroup['defaultActionHandler'](Action.BACK); 30 | 31 | expect(containerUiMock.hideUi).toHaveBeenCalled(); 32 | }); 33 | 34 | it('should not call hideUi on UIContainer on Action.SELECT', () => { 35 | rootNavigationGroup['defaultActionHandler'](Action.SELECT); 36 | 37 | expect(containerUiMock.hideUi).not.toHaveBeenCalled(); 38 | }) 39 | }); 40 | 41 | describe('handleNavigation', () => { 42 | it('should call showUi on UIContainer on navigation', () => { 43 | rootNavigationGroup.handleNavigation(Direction.DOWN); 44 | 45 | expect(containerUiMock.showUi).toHaveBeenCalled(); 46 | }); 47 | }); 48 | 49 | describe('release', () => { 50 | it('should clear up', () => { 51 | rootNavigationGroup.release(); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /spec/volumecontroller.spec.ts: -------------------------------------------------------------------------------- 1 | import { PlayerEvent, VolumeChangedEvent } from 'bitmovin-player'; 2 | import { VolumeController } from '../src/ts/volumecontroller'; 3 | import { MockHelper, TestingPlayerAPI } from './helper/MockHelper'; 4 | 5 | describe('VolumeController', () => { 6 | let playerMock: TestingPlayerAPI; 7 | let volumeController: VolumeController; 8 | 9 | beforeEach(() => { 10 | playerMock = MockHelper.getPlayerMock(); 11 | volumeController = new VolumeController(playerMock); 12 | }); 13 | 14 | describe('onChangedEvent', () => { 15 | 16 | it('should update the stored volume on VolumeChanged event', () => { 17 | volumeController.storeVolume = jest.fn(); 18 | 19 | playerMock.eventEmitter.fireEvent({ 20 | type: PlayerEvent.VolumeChanged, 21 | sourceVolume: 0.2, 22 | targetVolume: 0.7, 23 | timestamp: Date.now(), 24 | }); 25 | 26 | expect(volumeController.storeVolume).toHaveBeenCalledTimes(1); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/html/simple.html: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | Bitmovin Player UI Demo 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 31 | 32 | 33 |
34 |
35 | <- Back to playground 36 |
37 |
38 |
39 |
40 |
41 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/scss/bitmovinplayer-ui.scss: -------------------------------------------------------------------------------- 1 | @import 'skin-modern/skin'; 2 | -------------------------------------------------------------------------------- /src/scss/demo.scss: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 1em; 3 | } 4 | 5 | // sass-lint:disable no-ids 6 | #player { 7 | position: relative; 8 | } 9 | -------------------------------------------------------------------------------- /src/scss/skin-modern/_skin-ads.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | 3 | // sass-lint:disable nesting-depth 4 | .#{$prefix}-ui-skin-ads { 5 | 6 | .#{$prefix}-ui-ads-status { 7 | background-color: $color-background-bars; 8 | left: 1.5em; 9 | padding: .5em 1.5em; 10 | position: absolute; 11 | top: 1em; 12 | 13 | .#{$prefix}-ui-label-ad-message { 14 | @extend %ui-label; 15 | 16 | color: $color-secondary; 17 | white-space: normal; 18 | } 19 | 20 | .#{$prefix}-ui-button-ad-skip { 21 | @extend %ui-button; 22 | 23 | .#{$prefix}-label { 24 | display: inherit; 25 | 26 | &:hover { 27 | text-decoration: underline; 28 | } 29 | } 30 | 31 | // Add the dot between ad message and skip button 32 | &::before { 33 | color: $color-highlight; 34 | content: '●'; 35 | padding-left: .5em; 36 | padding-right: .5em; 37 | } 38 | } 39 | } 40 | 41 | /* Hide the huge playback button overlay while an ad is playing, so a click goes 42 | * through to the click-through overlay which will register the click and then 43 | * pause playback. In the paused state, the huge playback toggle button will be 44 | * shown and continues playback of the ad when clicked. 45 | */ 46 | &.#{$prefix}-player-state-playing { 47 | .#{$prefix}-ui-playbacktoggle-overlay { 48 | display: none; 49 | } 50 | } 51 | 52 | &.#{$prefix}-ui-skin-smallscreen { 53 | .#{$prefix}-ui-ads-status { 54 | bottom: 0; 55 | left: 0; 56 | padding: 1em 1.5em; 57 | top: auto; 58 | width: 100%; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/scss/skin-modern/_variables.scss: -------------------------------------------------------------------------------- 1 | $prefix: 'bmpui' !default; 2 | 3 | $color-black: #000 !default; 4 | $color-transparent: rgba(0, 0, 0, 0) !default; 5 | $color-red: #f00 !default; 6 | 7 | $color-highlight: #1fabe2 !default; //Bitmovin blue 8 | $color-primary: #fff !default; 9 | $color-secondary: #999 !default; 10 | 11 | $color-background: #111 !default; 12 | $color-background-highlight: transparentize(mix($color-black, $color-highlight, 75%), .3) !default; 13 | $color-background-bars: transparentize($color-black, .3) !default; 14 | $color-focus: #1b7fcc; 15 | 16 | $font-family: sans-serif !default; 17 | $font-size: 1em !default; 18 | 19 | $subtitle-text-color: #fff !default; 20 | $subtitle-text-border-color: #000 !default; 21 | 22 | $animation-duration: .3s !default; 23 | $animation-duration-short: $animation-duration * .5 !default; 24 | 25 | $focus-element-box-shadow: 0 0 0 2px rgba($color-focus, .8); 26 | -------------------------------------------------------------------------------- /src/scss/skin-modern/components/_airplaytogglebutton.scss: -------------------------------------------------------------------------------- 1 | @import '../variables'; 2 | @import '../mixins'; 3 | 4 | .#{$prefix}-ui-airplaytogglebutton { 5 | @extend %ui-button; 6 | 7 | background-image: url('../../assets/skin-modern/images/airplay.svg'); 8 | 9 | &:hover { 10 | @include svg-icon-shadow; 11 | } 12 | 13 | &.#{$prefix}-on { 14 | background-image: url('../../assets/skin-modern/images/airplayX.svg'); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/scss/skin-modern/components/_audiotracksettingspaneltogglebutton.scss: -------------------------------------------------------------------------------- 1 | // demo for extracted audio tracks and subtitle settings from the settings panel direct into the controlBar 2 | @import '../variables'; 3 | 4 | .#{$prefix}-ui-audiotracksettingstogglebutton { 5 | @extend %ui-settingstogglebutton; 6 | 7 | background-image: url('../../assets/skin-modern/images/audio-tracks.svg'); 8 | 9 | &.#{$prefix}-on { 10 | background-image: url('../../assets/skin-modern/images/audio-tracksX.svg'); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/scss/skin-modern/components/_bufferingoverlay.scss: -------------------------------------------------------------------------------- 1 | @import '../variables'; 2 | @import '../mixins'; 3 | 4 | // H/V center items in the middle of the overlay 5 | %center-items-in-overlay { 6 | display: table; 7 | 8 | > .#{$prefix}-container-wrapper { 9 | display: table-cell; 10 | text-align: center; 11 | vertical-align: middle; 12 | } 13 | } 14 | 15 | .#{$prefix}-ui-buffering-overlay { 16 | @extend %ui-container; 17 | @extend %center-items-in-overlay; 18 | 19 | @include layout-cover; 20 | @include hidden-animated($animation-duration * 2); 21 | 22 | background-color: $color-background-highlight; 23 | 24 | > .#{$prefix}-container-wrapper { 25 | padding: 3em; 26 | } 27 | 28 | a { 29 | color: $color-primary; 30 | 31 | &:hover, 32 | &:visited { 33 | color: $color-primary; 34 | } 35 | } 36 | 37 | .#{$prefix}-ui-buffering-overlay-indicator { 38 | $buffering-animation-duration: 2s; 39 | $buffering-animation-delay: $buffering-animation-duration * .1; 40 | 41 | @keyframes #{$prefix}-fancy { 42 | 0% { 43 | opacity: 0; 44 | transform: scale(1); 45 | } 46 | 47 | 20% { 48 | opacity: 1; 49 | } 50 | 51 | 30% { 52 | opacity: 1; 53 | } 54 | 55 | 50% { 56 | opacity: 0; 57 | transform: scale(2); 58 | } 59 | 60 | 100% { 61 | opacity: 0; 62 | transform: scale(3); 63 | } 64 | } 65 | 66 | animation: #{$prefix}-fancy $buffering-animation-duration ease-in infinite; 67 | background: url('../../assets/skin-modern/images/loader.svg') no-repeat center; 68 | display: inline-block; 69 | height: 2em; 70 | margin: .2em; 71 | width: 2em; 72 | 73 | @for $i from 1 through 3 { 74 | &:nth-child(#{$i}) { 75 | animation-delay: $buffering-animation-delay * ($i - 1); 76 | } 77 | } 78 | } 79 | 80 | &.#{$prefix}-hidden { 81 | .#{$prefix}-ui-buffering-overlay-indicator { 82 | display: none; 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/scss/skin-modern/components/_button.scss: -------------------------------------------------------------------------------- 1 | @import '../variables'; 2 | 3 | %ui-button { 4 | @extend %ui-component; 5 | 6 | background-color: transparent; 7 | background-origin: content-box; 8 | background-position: center; 9 | background-repeat: no-repeat; 10 | background-size: 1.5em; 11 | border: 0; 12 | box-sizing: content-box; 13 | cursor: pointer; 14 | font-size: 1em; 15 | height: 1.5em; 16 | min-width: 1.5em; 17 | padding: .25em; 18 | 19 | .#{$prefix}-label { 20 | color: $color-primary; 21 | display: none; 22 | } 23 | 24 | &.#{$prefix}-disabled { 25 | cursor: default; 26 | 27 | &, 28 | > * { 29 | pointer-events: none; 30 | } 31 | 32 | .#{$prefix}-label { 33 | &:hover { 34 | text-decoration: none; 35 | } 36 | } 37 | } 38 | 39 | @include hidden; 40 | @include focusable; 41 | } 42 | 43 | .#{$prefix}-ui-button { 44 | @extend %ui-button; 45 | } 46 | -------------------------------------------------------------------------------- /src/scss/skin-modern/components/_caststatusoverlay.scss: -------------------------------------------------------------------------------- 1 | @import '../variables'; 2 | @import '../mixins'; 3 | 4 | .#{$prefix}-ui-cast-status-overlay { 5 | @extend %ui-container; 6 | 7 | @include layout-cover; 8 | @include hidden-animated; 9 | 10 | background: $color-background url('../../assets/skin-modern/images/chromecast.svg') center no-repeat; 11 | background-size: 7em 7em; 12 | 13 | .#{$prefix}-ui-cast-status-label { 14 | color: $color-primary; 15 | font-size: 1.2em; 16 | left: 0; 17 | margin: 0 2em; 18 | pointer-events: none; 19 | position: absolute; 20 | right: 0; 21 | text-align: center; 22 | top: 65%; 23 | 24 | * { 25 | pointer-events: none; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/scss/skin-modern/components/_casttogglebutton.scss: -------------------------------------------------------------------------------- 1 | @import '../variables'; 2 | @import '../mixins'; 3 | 4 | .#{$prefix}-ui-casttogglebutton { 5 | @extend %ui-button; 6 | 7 | background-image: url('../../assets/skin-modern/images/chromecast.svg'); 8 | 9 | &:hover { 10 | @include svg-icon-shadow; 11 | } 12 | 13 | &.#{$prefix}-on { 14 | background-image: url('../../assets/skin-modern/images/chromecastX.svg'); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/scss/skin-modern/components/_clickoverlay.scss: -------------------------------------------------------------------------------- 1 | @import '../variables'; 2 | 3 | %ui-clickoverlay { 4 | @extend %ui-button; 5 | } 6 | 7 | .#{$prefix}-ui-clickoverlay { 8 | @extend %ui-clickoverlay; 9 | 10 | @include layout-cover; 11 | } 12 | -------------------------------------------------------------------------------- /src/scss/skin-modern/components/_closebutton.scss: -------------------------------------------------------------------------------- 1 | @import '../variables'; 2 | @import '../mixins'; 3 | 4 | .#{$prefix}-ui-closebutton { 5 | @extend %ui-button; 6 | 7 | @keyframes #{$prefix}-pulsate { 8 | 20% { 9 | transform: scale(1.1); 10 | } 11 | 12 | 40% { 13 | transform: scale(1); 14 | } 15 | 16 | 60% { 17 | transform: scale(1.1); 18 | } 19 | 20 | 80% { 21 | transform: scale(1); 22 | } 23 | } 24 | 25 | background-image: url('../../assets/skin-modern/images/close.svg'); 26 | 27 | &:hover { 28 | @include svg-icon-shadow; 29 | 30 | animation: #{$prefix}-pulsate 1s; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/scss/skin-modern/components/_component.scss: -------------------------------------------------------------------------------- 1 | @import '../variables'; 2 | 3 | %ui-component { 4 | /*! placeholder to avoid removal of empty selector */ 5 | //outline: 1px solid red; 6 | outline: 0; 7 | } 8 | 9 | .#{$prefix}-ui-component { 10 | @extend %ui-component; 11 | } 12 | -------------------------------------------------------------------------------- /src/scss/skin-modern/components/_container.scss: -------------------------------------------------------------------------------- 1 | @import '../variables'; 2 | 3 | %ui-container { 4 | @extend %ui-component; 5 | 6 | font-size: 1em; 7 | } 8 | 9 | .#{$prefix}-ui-container { 10 | @extend %ui-container; 11 | } 12 | -------------------------------------------------------------------------------- /src/scss/skin-modern/components/_controlbar.scss: -------------------------------------------------------------------------------- 1 | @import '../variables'; 2 | @import '../mixins'; 3 | 4 | .#{$prefix}-ui-controlbar { 5 | @extend %ui-container; 6 | 7 | @include hidden-animated-focusable; 8 | @include layout-align-bottom; 9 | 10 | background: linear-gradient(to bottom, $color-transparent, $color-background-bars); 11 | box-sizing: border-box; 12 | line-height: 1em; 13 | padding: 1em 1em .5em; 14 | 15 | .#{$prefix}-controlbar-top, 16 | .#{$prefix}-controlbar-bottom { 17 | > .#{$prefix}-container-wrapper { 18 | display: flex; 19 | margin: .5em 0; 20 | } 21 | } 22 | 23 | .#{$prefix}-controlbar-top { 24 | .#{$prefix}-ui-label { 25 | font-size: .9em; 26 | } 27 | 28 | > .#{$prefix}-container-wrapper > * { 29 | margin: 0 .5em; 30 | } 31 | } 32 | 33 | .#{$prefix}-controlbar-bottom { 34 | white-space: nowrap; // Required for iOS 8.2 to avoid wrapped controlbar due to wrong size calculation 35 | 36 | > .#{$prefix}-container-wrapper { 37 | 38 | .#{$prefix}-ui-volumeslider { 39 | margin: auto .5em; 40 | width: 5em; 41 | } 42 | } 43 | } 44 | } 45 | 46 | // IE9 compatibility: fallback for missing flexbox support 47 | // sass-lint:disable nesting-depth 48 | .#{$prefix}-no-flexbox { 49 | .#{$prefix}-ui-controlbar { 50 | .#{$prefix}-controlbar-top, 51 | .#{$prefix}-controlbar-bottom { 52 | > .#{$prefix}-container-wrapper { 53 | border-spacing: .5em 0; 54 | display: table; 55 | 56 | > * { 57 | @include hidden; // Add hidden here too, else it is overwritten by display: table-cell 58 | 59 | display: table-cell; 60 | vertical-align: middle; 61 | } 62 | 63 | .#{$prefix}-ui-volumeslider { 64 | width: 10%; 65 | } 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/scss/skin-modern/components/_ecomodetogglebutton.scss: -------------------------------------------------------------------------------- 1 | @import '../variables'; 2 | @import '../mixins'; 3 | 4 | .#{$prefix}-ui-ecomodetogglebutton { 5 | @extend %ui-button; 6 | height: 1em; 7 | min-width: 5em; 8 | 9 | &:hover { 10 | @include svg-icon-shadow; 11 | } 12 | 13 | &.#{$prefix}-on { 14 | background-image: url('../../assets/skin-modern/images/toggleOn.svg'); 15 | background-position: 20px center; 16 | background-size: 45% auto; 17 | margin-left: 2%; 18 | } 19 | 20 | &.#{$prefix}-off { 21 | background-image: url('../../assets/skin-modern/images/toggleOff.svg'); 22 | background-position: 20px center; 23 | background-size: 45% auto; 24 | } 25 | } 26 | 27 | #ecomodelabel::before { 28 | background-image: url('../../assets/skin-modern/images/leaf.svg'); 29 | background-repeat: no-repeat; 30 | background-size: 1.7em auto; 31 | content: ' '; 32 | display: inline-block; 33 | height: 1.5em; 34 | width: 2em; 35 | } 36 | -------------------------------------------------------------------------------- /src/scss/skin-modern/components/_errormessageoverlay.scss: -------------------------------------------------------------------------------- 1 | @import '../variables'; 2 | @import '../mixins'; 3 | 4 | .#{$prefix}-ui-errormessage-overlay { 5 | @extend %ui-container; 6 | 7 | @include layout-cover; 8 | @include hidden; 9 | 10 | background-color: $color-background; 11 | pointer-events: none; 12 | 13 | .#{$prefix}-ui-errormessage-label { 14 | color: $color-primary; 15 | font-size: 1.2em; 16 | left: 3em; 17 | position: absolute; 18 | right: 3em; 19 | text-align: center; 20 | user-select: text; 21 | white-space: pre-line; // enable linebreak in text 22 | 23 | // Vertically center the label 24 | & { 25 | // sass-lint:disable no-vendor-prefixes 26 | -ms-transform: translateY(-50%); // required for IE9 27 | top: 50%; 28 | transform: translateY(-50%); 29 | } 30 | 31 | ul { 32 | color: $color-secondary; 33 | font-size: .9em; 34 | padding: 0; 35 | 36 | li { 37 | list-style: none; 38 | } 39 | } 40 | } 41 | 42 | .#{$prefix}-ui-tvnoisecanvas { 43 | @include layout-cover; 44 | 45 | filter: blur(4px); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/scss/skin-modern/components/_fullscreentogglebutton.scss: -------------------------------------------------------------------------------- 1 | @import '../variables'; 2 | @import '../mixins'; 3 | 4 | .#{$prefix}-ui-fullscreentogglebutton { 5 | @extend %ui-button; 6 | 7 | background-image: url('../../assets/skin-modern/images/fullscreen.svg'); 8 | 9 | &:hover { 10 | @include svg-icon-shadow; 11 | } 12 | 13 | &.#{$prefix}-on { 14 | background-image: url('../../assets/skin-modern/images/fullscreenX.svg'); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/scss/skin-modern/components/_hugeplaybacktogglebutton.scss: -------------------------------------------------------------------------------- 1 | @import '../variables'; 2 | 3 | .#{$prefix}-ui-hugeplaybacktogglebutton { 4 | @extend %ui-button; 5 | 6 | @keyframes #{$prefix}-fade-out { 7 | from { 8 | opacity: 1; 9 | visibility: visible; 10 | } 11 | 12 | to { 13 | opacity: 0; 14 | transform: scale(2); 15 | visibility: hidden; 16 | } 17 | } 18 | 19 | @keyframes #{$prefix}-fade-in { 20 | from { 21 | opacity: 0; 22 | transform: scale(2); 23 | visibility: visible; 24 | } 25 | 26 | to { 27 | opacity: 1; 28 | } 29 | } 30 | 31 | @keyframes #{$prefix}-breathe { 32 | 30% { 33 | transform: scale(1.1); 34 | } 35 | 36 | 60% { 37 | transform: scale(1); 38 | } 39 | } 40 | 41 | cursor: default; 42 | height: 8em; 43 | outline: none; 44 | overflow: hidden; // hide overflow from scale animation 45 | width: 8em; 46 | 47 | .#{$prefix}-image { 48 | background-image: url('../../assets/skin-modern/images/play_big.svg'); 49 | background-position: center; 50 | background-repeat: no-repeat; 51 | background-size: 7em; 52 | height: 100%; 53 | width: 100%; 54 | 55 | &:hover { 56 | animation: #{$prefix}-breathe 3s ease-in-out infinite; 57 | } 58 | } 59 | 60 | &.#{$prefix}-on { 61 | .#{$prefix}-image { 62 | animation: #{$prefix}-fade-out $animation-duration cubic-bezier(.55, .055, .675, .19); // http://easings.net/de#easeInCubic 63 | transition: visibility 0s $animation-duration; 64 | visibility: hidden; 65 | } 66 | } 67 | 68 | &.#{$prefix}-off { 69 | .#{$prefix}-image { 70 | animation: #{$prefix}-fade-in $animation-duration cubic-bezier(.55, .055, .675, .19); // http://easings.net/de#easeInCubic 71 | visibility: visible; 72 | } 73 | } 74 | 75 | &.#{$prefix}-no-transition-animations { 76 | &.#{$prefix}-on, 77 | &.#{$prefix}-off { 78 | .#{$prefix}-image { 79 | animation: none; 80 | transition: none; 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/scss/skin-modern/components/_hugereplaybutton.scss: -------------------------------------------------------------------------------- 1 | @import '../variables'; 2 | @import '../mixins'; 3 | 4 | .#{$prefix}-ui-hugereplaybutton { 5 | @extend %ui-button; 6 | 7 | height: 5em; 8 | outline: none; 9 | width: 5em; 10 | 11 | .#{$prefix}-image { 12 | background-image: url('../../assets/skin-modern/images/replayX.svg'); 13 | background-position: center; 14 | background-repeat: no-repeat; 15 | background-size: 5em; 16 | height: 100%; 17 | width: 100%; 18 | 19 | @keyframes #{$prefix}-spin { 20 | 50% { 21 | transform: rotate(180deg) scale(1.1); 22 | } 23 | 24 | 100% { 25 | transform: rotate(360deg) scale(1); 26 | } 27 | } 28 | 29 | &:hover { 30 | animation: #{$prefix}-spin .5s ease-in; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/scss/skin-modern/components/_label.scss: -------------------------------------------------------------------------------- 1 | @import '../variables'; 2 | 3 | %ui-label { 4 | @extend %ui-component; 5 | 6 | @include hidden; 7 | 8 | cursor: default; 9 | white-space: nowrap; 10 | } 11 | 12 | .#{$prefix}-ui-label { 13 | @extend %ui-label; 14 | } 15 | 16 | .#{$prefix}-ui-label-savedEnergy { 17 | @extend %ui-label; 18 | font-size: 0.8em; 19 | color: #1fabe2; 20 | margin-left: 2.2em; 21 | } 22 | -------------------------------------------------------------------------------- /src/scss/skin-modern/components/_listbox.scss: -------------------------------------------------------------------------------- 1 | @import '../variables'; 2 | @import '../mixins'; 3 | 4 | %ui-listbox { 5 | @extend %ui-container; 6 | 7 | .#{$prefix}-ui-listbox-button { 8 | @extend %ui-button; 9 | 10 | box-sizing: border-box; 11 | display: block; 12 | font-size: .8em; 13 | height: 100%; 14 | min-width: 10em; 15 | padding: .5em; 16 | width: 100%; 17 | 18 | .#{$prefix}-label { 19 | display: inherit; 20 | } 21 | 22 | &.#{$prefix}-selected { 23 | background-color: transparentize($color-highlight, .3); 24 | } 25 | 26 | &:hover { 27 | background-color: transparentize($color-highlight, .15); 28 | } 29 | 30 | &:last-child { 31 | border-bottom: 0; 32 | } 33 | } 34 | } 35 | 36 | .#{$prefix}-ui-listbox { 37 | @extend %ui-listbox; 38 | } 39 | -------------------------------------------------------------------------------- /src/scss/skin-modern/components/_pictureinpicturetogglebutton.scss: -------------------------------------------------------------------------------- 1 | @import '../variables'; 2 | @import '../mixins'; 3 | 4 | .#{$prefix}-ui-piptogglebutton { 5 | @extend %ui-button; 6 | 7 | background-image: url('../../assets/skin-modern/images/picinpic1.svg'); 8 | 9 | &:hover { 10 | @include svg-icon-shadow; 11 | } 12 | 13 | &.#{$prefix}-on { 14 | background-image: url('../../assets/skin-modern/images/picinpic1X.svg'); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/scss/skin-modern/components/_playbacktimelabel.scss: -------------------------------------------------------------------------------- 1 | @import '../variables'; 2 | 3 | .#{$prefix}-ui-playbacktimelabel { 4 | @extend %ui-label; 5 | 6 | text-transform: uppercase; 7 | 8 | &.#{$prefix}-ui-playbacktimelabel-live { 9 | cursor: pointer; 10 | 11 | &::before { 12 | color: $color-secondary; 13 | content: '●'; 14 | padding-right: .2em; 15 | } 16 | 17 | &.#{$prefix}-ui-playbacktimelabel-live-edge { 18 | &::before { 19 | color: $color-red; 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/scss/skin-modern/components/_playbacktogglebutton.scss: -------------------------------------------------------------------------------- 1 | @import '../variables'; 2 | @import '../mixins'; 3 | 4 | .#{$prefix}-ui-playbacktogglebutton { 5 | @extend %ui-button; 6 | 7 | background-image: url('../../assets/skin-modern/images/play.svg'); 8 | 9 | &:hover { 10 | @include svg-icon-shadow; 11 | } 12 | 13 | &.#{$prefix}-on { 14 | background-image: url('../../assets/skin-modern/images/pause.svg'); 15 | 16 | &.#{$prefix}-stoptoggle { 17 | background-image: url('../../assets/skin-modern/images/stop.svg'); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/scss/skin-modern/components/_playbacktoggleoverlay.scss: -------------------------------------------------------------------------------- 1 | @import '../variables'; 2 | @import '../mixins'; 3 | 4 | .#{$prefix}-ui-playbacktoggle-overlay { 5 | @extend %ui-container; 6 | 7 | .#{$prefix}-ui-hugeplaybacktogglebutton { 8 | @include layout-cover; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/scss/skin-modern/components/_quickseekbutton.scss: -------------------------------------------------------------------------------- 1 | @import '../variables'; 2 | @import '../mixins'; 3 | 4 | .#{$prefix}-ui-quickseekbutton { 5 | @extend %ui-button; 6 | 7 | &:hover { 8 | @include svg-icon-shadow; 9 | } 10 | 11 | &[data-#{$prefix}-seek-direction='forward'] { 12 | background-image: url('../../assets/skin-modern/images/quickseek-fastforward.svg'); 13 | } 14 | 15 | &[data-#{$prefix}-seek-direction='rewind'] { 16 | background-image: url('../../assets/skin-modern/images/quickseek-rewind.svg'); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/scss/skin-modern/components/_recommendationoverlay.scss: -------------------------------------------------------------------------------- 1 | @import '../variables'; 2 | @import '../mixins'; 3 | 4 | .#{$prefix}-ui-recommendation-overlay { 5 | @extend %ui-container; 6 | 7 | @include layout-cover; 8 | @include layout-center-children-in-container; 9 | @include hidden-animated($animation-duration * 2); 10 | 11 | background-color: $color-background-highlight; 12 | 13 | > .#{$prefix}-container-wrapper { 14 | padding: 3em; 15 | } 16 | 17 | a { 18 | color: $color-primary; 19 | 20 | &:hover, 21 | &:visited { 22 | color: $color-primary; 23 | } 24 | } 25 | 26 | &.#{$prefix}-recommendations { 27 | .#{$prefix}-ui-hugereplaybutton { 28 | bottom: 2em; 29 | left: 2em; 30 | position: absolute; 31 | } 32 | 33 | .#{$prefix}-ui-recommendation-item { 34 | $margin: 1em; 35 | $item-scale: 1; 36 | 37 | background-position: center; 38 | background-size: cover; 39 | display: inline-block; 40 | font-size: .7em; 41 | height: (9em * $item-scale); 42 | margin: .3em .6em; 43 | overflow: hidden; 44 | position: relative; 45 | text-align: left; 46 | text-shadow: 0 0 3px $color-background; 47 | transform: scale(1); 48 | transition: transform $animation-duration-short ease-out; 49 | width: (16em * $item-scale); 50 | 51 | .#{$prefix}-background { 52 | background: linear-gradient(to bottom, $color-transparent, $color-transparent, $color-background-bars); 53 | height: 100%; 54 | position: absolute; 55 | top: 20%; 56 | transition: top $animation-duration-short ease-out; 57 | width: 100%; 58 | } 59 | 60 | .#{$prefix}-title { 61 | bottom: $margin + 2em; 62 | left: $margin; 63 | position: absolute; 64 | right: $margin; 65 | 66 | .#{$prefix}-innertitle { 67 | font-size: 1.2em; 68 | white-space: normal; 69 | word-break: break-all; 70 | } 71 | } 72 | 73 | .#{$prefix}-duration { 74 | bottom: $margin; 75 | left: $margin; 76 | position: absolute; 77 | } 78 | 79 | &:hover { 80 | outline: 2px solid $color-highlight; 81 | transform: scale(1.05); 82 | transition: transform $animation-duration-short ease-in; 83 | 84 | .#{$prefix}-background { 85 | top: 0; 86 | transition: top $animation-duration-short ease-in; 87 | } 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/scss/skin-modern/components/_replaybutton.scss: -------------------------------------------------------------------------------- 1 | @import '../variables'; 2 | @import '../mixins'; 3 | 4 | .#{$prefix}-ui-replaybutton { 5 | @extend %ui-button; 6 | 7 | background-image: url('../../assets/skin-modern/images/replay-nocircle.svg'); 8 | 9 | &:hover { 10 | @include svg-icon-shadow; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/scss/skin-modern/components/_seekbarlabel.scss: -------------------------------------------------------------------------------- 1 | @import '../variables'; 2 | 3 | .#{$prefix}-ui-seekbar-label { 4 | @extend %ui-container; 5 | 6 | @include hidden-animated; 7 | 8 | bottom: 100%; 9 | left: 0; 10 | margin-bottom: 1em; 11 | pointer-events: none; 12 | position: absolute; 13 | text-align: center; 14 | 15 | // Center container on left edge to get it centered over timeline position 16 | %center-on-left-edge { 17 | margin-left: -50%; 18 | margin-right: 50%; 19 | position: relative; 20 | } 21 | 22 | > .#{$prefix}-container-wrapper { 23 | @extend %center-on-left-edge; 24 | 25 | padding-left: 1em; 26 | padding-right: 1em; 27 | } 28 | 29 | // bottom arrow from http://www.cssarrowplease.com/ 30 | .#{$prefix}-seekbar-label-caret { 31 | border: solid transparent; 32 | border-color: transparent; 33 | border-top-color: $color-primary; 34 | border-width: .5em; 35 | height: 0; 36 | margin-left: -.5em; 37 | pointer-events: none; 38 | position: absolute; 39 | top: 100%; 40 | width: 0; 41 | } 42 | 43 | .#{$prefix}-seekbar-label-inner { 44 | border-bottom: .2em solid $color-primary; 45 | 46 | > .#{$prefix}-container-wrapper { 47 | position: relative; 48 | 49 | .#{$prefix}-seekbar-thumbnail { 50 | width: 6em; 51 | } 52 | 53 | .#{$prefix}-seekbar-label-metadata { 54 | background: linear-gradient(to bottom, $color-transparent, $color-background-bars); 55 | bottom: 0; 56 | box-sizing: border-box; 57 | display: block; 58 | padding: .5em; 59 | position: absolute; 60 | width: 100%; 61 | 62 | .#{$prefix}-seekbar-label-time { 63 | display: block; 64 | line-height: .8em; 65 | } 66 | 67 | .#{$prefix}-seekbar-label-title { 68 | display: block; 69 | margin-bottom: .3em; 70 | white-space: normal; 71 | } 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/scss/skin-modern/components/_selectbox.scss: -------------------------------------------------------------------------------- 1 | @import '../variables'; 2 | @import '../mixins'; 3 | 4 | .#{$prefix}-ui-selectbox { 5 | @extend %ui-component; 6 | 7 | @include focusable; 8 | 9 | background-color: transparent; 10 | border: 0; 11 | color: $color-highlight; 12 | cursor: pointer; 13 | font-size: .8em; 14 | padding: .3em; 15 | 16 | option { 17 | color: $color-secondary; 18 | 19 | &:checked { 20 | color: $color-highlight; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/scss/skin-modern/components/_settingspanel.scss: -------------------------------------------------------------------------------- 1 | @import '../variables'; 2 | @import '../mixins'; 3 | 4 | %ui-settings-panel { 5 | @extend %ui-container; 6 | 7 | @include hidden-animated-with-additional-transitions($animation-duration, 8 | ( 9 | height: (.35s, cubic-bezier(.4, 0, .2, 1)), 10 | width: (.35s, cubic-bezier(.4, 0, .2, 1)) 11 | ) 12 | ); 13 | 14 | $background-color: transparentize($color-background, .15); 15 | 16 | background-color: $background-color; 17 | bottom: 5em; 18 | overflow: hidden; 19 | padding: 0; 20 | position: absolute; 21 | right: 2em; 22 | 23 | > .#{$prefix}-container-wrapper { 24 | margin: .5em; 25 | overflow-y: auto; 26 | 27 | > * { 28 | margin: 0 .5em; 29 | } 30 | } 31 | } 32 | 33 | .#{$prefix}-ui-settings-panel { 34 | @extend %ui-settings-panel; 35 | } 36 | 37 | // Remove margin inherited from controlbar 38 | .#{$prefix}-container-wrapper > .#{$prefix}-ui-settings-panel { 39 | margin: 0; 40 | } 41 | -------------------------------------------------------------------------------- /src/scss/skin-modern/components/_settingspanelpage.scss: -------------------------------------------------------------------------------- 1 | @import '../variables'; 2 | 3 | %ui-settings-panel-page { 4 | display: none; 5 | 6 | &.#{$prefix}-active { 7 | display: block; 8 | } 9 | 10 | // A "line" in the panel: a container holding a label + control 11 | .#{$prefix}-container-wrapper > * { 12 | // Labels 13 | &.#{$prefix}-ui-label { 14 | display: inline-block; 15 | font-size: .8em; 16 | width: 45%; 17 | } 18 | 19 | // Controls (e.g. selectbox) 20 | &.#{$prefix}-ui-selectbox { 21 | margin-left: 10%; 22 | width: 45%; 23 | } 24 | } 25 | 26 | .#{$prefix}-ui-settings-panel-item { 27 | border-bottom: 1px solid transparentize($color-secondary, .7); 28 | padding: .5em 0; 29 | white-space: nowrap; 30 | 31 | &.#{$prefix}-last { 32 | border-bottom: 0; 33 | } 34 | 35 | &.#{$prefix}-hidden { 36 | display: none; 37 | } 38 | } 39 | } 40 | 41 | .#{$prefix}-ui-settings-panel-page { 42 | @extend %ui-settings-panel-page; 43 | } 44 | -------------------------------------------------------------------------------- /src/scss/skin-modern/components/_settingspanelpagebackbutton.scss: -------------------------------------------------------------------------------- 1 | @import '../variables'; 2 | 3 | %ui-settingspanelpagebackbutton { 4 | @extend %ui-button; 5 | 6 | font-size: .8em; 7 | position: relative; 8 | width: 8em; 9 | 10 | .#{$prefix}-label { 11 | display: inline-block; 12 | 13 | &::before { 14 | border-bottom: .2em solid $color-primary; 15 | border-left: .2em solid $color-primary; 16 | content: ''; 17 | height: .6em; 18 | margin-left: -.8em; 19 | position: absolute; 20 | top: .6em; 21 | transform: rotate(45deg); 22 | width: .6em; 23 | } 24 | } 25 | } 26 | 27 | .#{$prefix}-ui-settingspanelpagebackbutton { 28 | @extend %ui-settingspanelpagebackbutton; 29 | } 30 | -------------------------------------------------------------------------------- /src/scss/skin-modern/components/_settingspanelpageopenbutton.scss: -------------------------------------------------------------------------------- 1 | @import '../variables'; 2 | @import '../mixins'; 3 | 4 | %ui-settingspanelpageopenbutton { 5 | @extend %ui-button; 6 | 7 | background-image: url('../../assets/skin-modern/images/settings.svg'); 8 | max-height: .8em; 9 | padding: .3em 0; 10 | vertical-align: bottom; 11 | 12 | &:hover { 13 | @include svg-icon-shadow; 14 | } 15 | 16 | &.#{$prefix}-on { 17 | background-image: url('../../assets/skin-modern/images/settingsX.svg'); 18 | } 19 | } 20 | 21 | .#{$prefix}-ui-settingspanelpageopenbutton { 22 | @extend %ui-settingspanelpageopenbutton; 23 | } 24 | -------------------------------------------------------------------------------- /src/scss/skin-modern/components/_settingstogglebutton.scss: -------------------------------------------------------------------------------- 1 | @import '../variables'; 2 | @import '../mixins'; 3 | 4 | %ui-settingstogglebutton { 5 | @extend %ui-button; 6 | 7 | &:hover { 8 | @include svg-icon-shadow; 9 | } 10 | 11 | &.#{$prefix}-on { 12 | &:hover { 13 | @include svg-icon-on-shadow; 14 | } 15 | } 16 | } 17 | 18 | .#{$prefix}-ui-settingstogglebutton { 19 | @extend %ui-settingstogglebutton; 20 | 21 | background-image: url('../../assets/skin-modern/images/settings.svg'); 22 | 23 | &.#{$prefix}-on { 24 | background-image: url('../../assets/skin-modern/images/settingsX.svg'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/scss/skin-modern/components/_spacer.scss: -------------------------------------------------------------------------------- 1 | @import '../variables'; 2 | 3 | %ui-spacer { 4 | @extend %ui-component; 5 | 6 | height: 100%; 7 | width: 100%; 8 | } 9 | 10 | .#{$prefix}-ui-spacer { 11 | @extend %ui-spacer; 12 | } 13 | -------------------------------------------------------------------------------- /src/scss/skin-modern/components/_subtitleoverlay-cea608.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:math'; 2 | 3 | .#{$prefix}-ui-subtitle-overlay { 4 | --cea608-row-height: math.div(100%, 15); 5 | 6 | &.#{$prefix}-cea608 { 7 | 8 | bottom: 2em; 9 | left: 3em; 10 | right: 3em; 11 | top: 2em; 12 | 13 | .#{$prefix}-subtitle-region-container { 14 | height: var(--cea608-row-height); 15 | left: 0; 16 | line-height: 1em; 17 | right: 0; 18 | text-align: left; 19 | 20 | // Define positions for all 15 rows 21 | @for $i from 0 through 14 { 22 | &.#{$prefix}-subtitle-position-cea608-row-#{$i} { 23 | top: calc(var(--cea608-row-height) * #{$i}); 24 | } 25 | } 26 | } 27 | 28 | .#{$prefix}-ui-subtitle-label { 29 | display: inline-block; 30 | font-family: 'Courier New', Courier, 'Nimbus Mono L', 'Cutive Mono', monospace; 31 | position: absolute; 32 | text-transform: uppercase; 33 | vertical-align: bottom; 34 | 35 | // sass-lint:disable force-pseudo-nesting nesting-depth 36 | &:nth-child(1n-1)::after { 37 | content: normal; 38 | white-space: normal; 39 | } 40 | } 41 | 42 | &.#{$prefix}-controlbar-visible { 43 | // Disable the make-space-for-controlbar mechanism 44 | // We don't want CEA-608 subtitles to make space for the controlbar because they're 45 | // positioned absolutely in relation to the video picture and thus cannot just move 46 | // somewhere else. 47 | bottom: 2em; 48 | transition: none; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/scss/skin-modern/components/_subtitleoverlay.scss: -------------------------------------------------------------------------------- 1 | @import '../variables'; 2 | @import '../mixins'; 3 | 4 | .#{$prefix}-ui-uicontainer { 5 | .#{$prefix}-ui-subtitle-overlay { 6 | @extend %ui-container; 7 | 8 | @include hidden; 9 | 10 | bottom: 0; 11 | font-size: 1.2em; 12 | left: 0; 13 | pointer-events: none; 14 | position: absolute; 15 | right: 0; 16 | text-align: center; 17 | top: 0; 18 | transition: bottom $animation-duration-short ease-out; 19 | 20 | * { 21 | // This aims to prevent possibly conflicting style definitions inherited 22 | // from target applications which can break subtitles styling. It's still possible 23 | // to override this with selector of higher priority score. 24 | all: unset; 25 | } 26 | 27 | p { 28 | // It may happen that we render

inside of an and the `all: unset;` reseting above sets 29 | // p to inherit the inline display instead of its default display block so this sets it back. 30 | display: block; 31 | } 32 | 33 | .#{$prefix}-subtitle-region-container { 34 | position: absolute; 35 | 36 | &.#{$prefix}-subtitle-position-default { 37 | bottom: 2em; 38 | left: 3em; 39 | right: 3em; 40 | top: initial; 41 | } 42 | 43 | &.#{$prefix}-subtitle-position-bottom > div { 44 | bottom: 0; 45 | position: absolute; 46 | width: 100%; 47 | } 48 | } 49 | 50 | .#{$prefix}-ui-subtitle-label { 51 | @include text-border($subtitle-text-border-color); 52 | 53 | color: $subtitle-text-color; 54 | height: fit-content; 55 | 56 | // Break labels into separate lines 57 | // sass-lint:disable force-pseudo-nesting 58 | &:nth-child(1n-1)::after { 59 | content: '\A'; 60 | // VTT flex styling can increase this elements height, making the background larger 61 | height: 0; 62 | white-space: pre-line; 63 | width: 0; 64 | } 65 | } 66 | 67 | // Move the subtitle up above the controlbar when it appears to avoid them overlapping 68 | &.#{$prefix}-controlbar-visible { 69 | bottom: 5em; 70 | transition: bottom $animation-duration-short ease-in; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/scss/skin-modern/components/_subtitlesettingspaneltogglebutton.scss: -------------------------------------------------------------------------------- 1 | // demo for extracted audio tracks and subtitle settings from the settings panel direct into the controlBar 2 | @import '../variables'; 3 | 4 | .#{$prefix}-ui-subtitlesettingstogglebutton { 5 | @extend %ui-settingstogglebutton; 6 | 7 | background-image: url('../../assets/skin-modern/images/subtitles.svg'); 8 | 9 | &.#{$prefix}-on { 10 | background-image: url('../../assets/skin-modern/images/subtitlesX.svg'); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/scss/skin-modern/components/_titlebar.scss: -------------------------------------------------------------------------------- 1 | @import '../variables'; 2 | @import '../mixins'; 3 | 4 | .#{$prefix}-ui-titlebar { 5 | @extend %ui-container; 6 | 7 | @include hidden-animated-focusable; 8 | @include layout-align-top; 9 | 10 | background: linear-gradient(to top, $color-transparent, $color-background-bars); 11 | box-sizing: border-box; 12 | padding: .5em 1em 1em; 13 | pointer-events: none; 14 | 15 | > .#{$prefix}-container-wrapper { 16 | padding: .5em; 17 | pointer-events: none; 18 | 19 | .#{$prefix}-label-metadata { 20 | pointer-events: none; 21 | } 22 | 23 | .#{$prefix}-label-metadata-title { 24 | cursor: default; 25 | display: block; 26 | font-size: 1.2em; 27 | text-shadow: 0 0 5px $color-black; 28 | white-space: normal; 29 | } 30 | 31 | .#{$prefix}-label-metadata-description { 32 | color: lighten($color-secondary, 30%); 33 | cursor: default; 34 | display: block; 35 | text-shadow: 0 0 5px $color-black; 36 | white-space: normal; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/scss/skin-modern/components/_uicontainer.scss: -------------------------------------------------------------------------------- 1 | @import '../variables'; 2 | @import '../mixins'; 3 | 4 | .#{$prefix}-ui-uicontainer { 5 | @extend %ui-container; 6 | 7 | @include layout-cover; 8 | 9 | font-size: 1em; 10 | overflow: hidden; 11 | pointer-events: none; // Do not catch pointer events, pass them through 12 | 13 | * { 14 | pointer-events: auto; 15 | } 16 | 17 | &.#{$prefix}-hidden { 18 | // Most hiding within the UI works through the "visibility" property, because "display" cannot be animated. 19 | // For the outermost UI container we use "display" though, to not block any events (e.g. click events) on the video 20 | // when the UI is hidden. 21 | display: none; 22 | } 23 | 24 | // sass-lint:disable force-element-nesting 25 | &.#{$prefix}-player-state-playing.#{$prefix}-controls-hidden { 26 | // Hide cursor while the controls are hidden 27 | * { 28 | cursor: none; 29 | } 30 | } 31 | 32 | &.#{$prefix}-controls-shown { 33 | .#{$prefix}-ui-hugeplaybacktogglebutton { 34 | &:focus { 35 | box-shadow: inset -4px -3px 2px 9px $color-focus; 36 | } 37 | 38 | &:focus:not(.#{$prefix}-focus-visible) { 39 | box-shadow: none; 40 | } 41 | } 42 | } 43 | 44 | // IE9 compatibility: set transparent 1x1 pixel png background image to make it capture mouse events (IE9 does not capture events in areas without image or color content) 45 | // We abuse the no-flexbox class which is only set in IE9 (of all supported browsers) 46 | &.#{$prefix}-no-flexbox { 47 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='); 48 | 49 | // Fullscreen legacy mode for IE9 needs additional special care to get UI visible and spanned over viewport 50 | &.#{$prefix}-fullscreen { 51 | left: 0; 52 | position: fixed; 53 | top: 0; 54 | z-index: 999999; // render UI above player 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/scss/skin-modern/components/_volumecontrolbutton.scss: -------------------------------------------------------------------------------- 1 | @import '../variables'; 2 | @import '../mixins'; 3 | 4 | .#{$prefix}-ui-volumecontrolbutton { 5 | @extend %ui-container; 6 | 7 | line-height: 0; // Fix layout for Firefox: removes spurious space in the container 8 | position: relative; 9 | 10 | .#{$prefix}-ui-volumeslider { 11 | @include animate-slide-in-from-bottom(6em, $animation-duration-short); 12 | 13 | background-color: $color-background; 14 | bottom: 100%; 15 | height: 6em; 16 | position: absolute; 17 | width: 1.5em; 18 | 19 | .#{$prefix}-seekbar { 20 | bottom: .5em; 21 | height: auto; 22 | left: .3em; 23 | overflow: hidden; 24 | position: absolute; 25 | right: .3em; 26 | top: .5em; 27 | width: auto; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/scss/skin-modern/components/_volumeslider.scss: -------------------------------------------------------------------------------- 1 | @import '../variables'; 2 | @import '../mixins'; 3 | @import './seekbar'; 4 | 5 | .#{$prefix}-ui-volumeslider { 6 | @extend %ui-seekbar; 7 | 8 | .#{$prefix}-seekbar { 9 | .#{$prefix}-seekbar-playbackposition-marker { 10 | @include seekbar-position-marker($seekbar-height * 3 - .25em); 11 | background-color: $color-highlight; 12 | border: 0; 13 | } 14 | 15 | .#{$prefix}-seekbar-bufferlevel { 16 | display: none; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/scss/skin-modern/components/_volumetogglebutton.scss: -------------------------------------------------------------------------------- 1 | @import '../variables'; 2 | @import '../mixins'; 3 | 4 | .#{$prefix}-ui-volumetogglebutton { 5 | @extend %ui-button; 6 | 7 | &:hover { 8 | @include svg-icon-shadow; 9 | } 10 | 11 | &.#{$prefix}-muted { 12 | background-image: url('../../assets/skin-modern/images/music-off.svg'); 13 | } 14 | 15 | &.#{$prefix}-unmuted { 16 | &[data-#{$prefix}-volume-level-tens='0'] { 17 | background-image: url('../../assets/skin-modern/images/music-off.svg'); 18 | } 19 | 20 | &[data-#{$prefix}-volume-level-tens='1'], 21 | &[data-#{$prefix}-volume-level-tens='2'], 22 | &[data-#{$prefix}-volume-level-tens='3'], 23 | &[data-#{$prefix}-volume-level-tens='4'], 24 | &[data-#{$prefix}-volume-level-tens='5'] { 25 | background-image: url('../../assets/skin-modern/images/music-low.svg'); 26 | } 27 | 28 | &[data-#{$prefix}-volume-level-tens='6'], 29 | &[data-#{$prefix}-volume-level-tens='7'], 30 | &[data-#{$prefix}-volume-level-tens='8'], 31 | &[data-#{$prefix}-volume-level-tens='9'], 32 | &[data-#{$prefix}-volume-level-tens='10'] { 33 | background-image: url('../../assets/skin-modern/images/music-on.svg'); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/scss/skin-modern/components/_vrtogglebutton.scss: -------------------------------------------------------------------------------- 1 | @import '../variables'; 2 | @import '../mixins'; 3 | 4 | .#{$prefix}-ui-vrtogglebutton { 5 | @extend %ui-button; 6 | 7 | // svg() usage: http://pavliko.github.io/postcss-svg/ 8 | background-image: url('../../assets/skin-modern/images/glasses.svg'); 9 | 10 | &:hover { 11 | @include svg-icon-shadow; 12 | } 13 | 14 | &.#{$prefix}-on { 15 | background-image: url('../../assets/skin-modern/images/glassesX.svg'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/scss/skin-modern/components/_watermark.scss: -------------------------------------------------------------------------------- 1 | @import '../variables'; 2 | 3 | .#{$prefix}-ui-watermark { 4 | @extend %ui-clickoverlay; 5 | 6 | $watermark-size: 4em; 7 | 8 | background-image: url('../../assets/skin-modern/images/logo.svg'); 9 | background-size: initial; 10 | height: $watermark-size; 11 | margin: 2em; 12 | opacity: .8; 13 | position: absolute; 14 | right: 0; 15 | top: 0; 16 | width: $watermark-size; 17 | 18 | &:hover { 19 | opacity: 1; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/scss/skin-modern/components/subtitlesettings/_subtitlesettings.scss: -------------------------------------------------------------------------------- 1 | @import './subtitlesettingsresetbutton'; 2 | @import './subtitleoverlay-settings'; 3 | -------------------------------------------------------------------------------- /src/scss/skin-modern/components/subtitlesettings/_subtitlesettingsresetbutton.scss: -------------------------------------------------------------------------------- 1 | @import '../../variables'; 2 | 3 | .#{$prefix}-ui-subtitlesettingsresetbutton { 4 | @extend %ui-button; 5 | 6 | font-size: .8em; 7 | width: 12em; 8 | 9 | .#{$prefix}-label { 10 | display: inline-block; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/ts/arrayutils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @category Utils 3 | */ 4 | export namespace ArrayUtils { 5 | /** 6 | * Removes an item from an array. 7 | * @param array the array that may contain the item to remove 8 | * @param item the item to remove from the array 9 | * @returns {any} the removed item or null if it wasn't part of the array 10 | */ 11 | export function remove(array: T[], item: T): T | null { 12 | let index = array.indexOf(item); 13 | 14 | if (index > -1) { 15 | return array.splice(index, 1)[0]; 16 | } else { 17 | return null; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/ts/components/adclickoverlay.ts: -------------------------------------------------------------------------------- 1 | import { ClickOverlay, ClickOverlayConfig } from './clickoverlay'; 2 | import { UIInstanceManager } from '../uimanager'; 3 | import { Ad, AdEvent, PlayerAPI } from 'bitmovin-player'; 4 | 5 | /** 6 | * A simple click capture overlay for clickThroughUrls of ads. 7 | * 8 | * @category Components 9 | */ 10 | export class AdClickOverlay extends ClickOverlay { 11 | constructor(config: ClickOverlayConfig = {}) { 12 | super(config); 13 | 14 | this.config = this.mergeConfig(config, { 15 | acceptsTouchWithUiHidden: true, 16 | }, this.config); 17 | } 18 | 19 | configure(player: PlayerAPI, uimanager: UIInstanceManager): void { 20 | super.configure(player, uimanager); 21 | 22 | let clickThroughCallback: () => void = null; 23 | 24 | player.on(player.exports.PlayerEvent.AdStarted, (event: AdEvent) => { 25 | let ad = event.ad; 26 | this.setUrl(ad.clickThroughUrl); 27 | clickThroughCallback = ad.clickThroughUrlOpened; 28 | }); 29 | 30 | // Clear click-through URL when ad has finished 31 | let adFinishedHandler = () => { 32 | this.setUrl(null); 33 | }; 34 | 35 | player.on(player.exports.PlayerEvent.AdFinished, adFinishedHandler); 36 | player.on(player.exports.PlayerEvent.AdSkipped, adFinishedHandler); 37 | player.on(player.exports.PlayerEvent.AdError, adFinishedHandler); 38 | 39 | this.onClick.subscribe(() => { 40 | // Pause the ad when overlay is clicked 41 | player.pause('ui-ad-click-overlay'); 42 | 43 | if (clickThroughCallback) { 44 | clickThroughCallback(); 45 | } 46 | }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/ts/components/admessagelabel.ts: -------------------------------------------------------------------------------- 1 | import {Label, LabelConfig} from './label'; 2 | import {UIInstanceManager} from '../uimanager'; 3 | import {StringUtils} from '../stringutils'; 4 | import { AdEvent, LinearAd, PlayerAPI } from 'bitmovin-player'; 5 | import { i18n } from '../localization/i18n'; 6 | 7 | /** 8 | * A label that displays a message about a running ad, optionally with a countdown. 9 | * 10 | * @category Components 11 | */ 12 | export class AdMessageLabel extends Label { 13 | 14 | constructor(config: LabelConfig = {}) { 15 | super(config); 16 | 17 | this.config = this.mergeConfig(config, { 18 | cssClass: 'ui-label-ad-message', 19 | text: i18n.getLocalizer('ads.remainingTime') , 20 | }, this.config); 21 | } 22 | 23 | configure(player: PlayerAPI, uimanager: UIInstanceManager): void { 24 | super.configure(player, uimanager); 25 | 26 | let config = this.getConfig(); 27 | let text = config.text; 28 | 29 | let updateMessageHandler = () => { 30 | this.setText(StringUtils.replaceAdMessagePlaceholders(i18n.performLocalization(text), null, player)); 31 | }; 32 | 33 | let adStartHandler = (event: AdEvent) => { 34 | let uiConfig = (event.ad as LinearAd).uiConfig; 35 | text = uiConfig && uiConfig.message || config.text; 36 | 37 | updateMessageHandler(); 38 | 39 | player.on(player.exports.PlayerEvent.TimeChanged, updateMessageHandler); 40 | }; 41 | 42 | let adEndHandler = () => { 43 | player.off(player.exports.PlayerEvent.TimeChanged, updateMessageHandler); 44 | }; 45 | 46 | player.on(player.exports.PlayerEvent.AdStarted, adStartHandler); 47 | player.on(player.exports.PlayerEvent.AdSkipped, adEndHandler); 48 | player.on(player.exports.PlayerEvent.AdError, adEndHandler); 49 | player.on(player.exports.PlayerEvent.AdFinished, adEndHandler); 50 | } 51 | } -------------------------------------------------------------------------------- /src/ts/components/airplaytogglebutton.ts: -------------------------------------------------------------------------------- 1 | import {ToggleButton, ToggleButtonConfig} from './togglebutton'; 2 | import {UIInstanceManager} from '../uimanager'; 3 | import { PlayerAPI } from 'bitmovin-player'; 4 | import { i18n } from '../localization/i18n'; 5 | 6 | /** 7 | * A button that toggles Apple AirPlay. 8 | * 9 | * @category Buttons 10 | */ 11 | export class AirPlayToggleButton extends ToggleButton { 12 | 13 | constructor(config: ToggleButtonConfig = {}) { 14 | super(config); 15 | 16 | this.config = this.mergeConfig(config, { 17 | cssClass: 'ui-airplaytogglebutton', 18 | text: i18n.getLocalizer('appleAirplay'), 19 | }, this.config); 20 | } 21 | 22 | configure(player: PlayerAPI, uimanager: UIInstanceManager): void { 23 | super.configure(player, uimanager); 24 | 25 | if (!player.isAirplayAvailable) { 26 | // If the player does not support Airplay (player 7.0), we just hide this component and skip configuration 27 | this.hide(); 28 | return; 29 | } 30 | 31 | this.onClick.subscribe(() => { 32 | if (player.isAirplayAvailable()) { 33 | player.showAirplayTargetPicker(); 34 | } else { 35 | if (console) { 36 | console.log('AirPlay unavailable'); 37 | } 38 | } 39 | }); 40 | 41 | const airPlayAvailableHandler = () => { 42 | if (player.isAirplayAvailable()) { 43 | this.show(); 44 | } else { 45 | this.hide(); 46 | } 47 | }; 48 | 49 | const airPlayChangedHandler = () => { 50 | if (player.isAirplayActive()) { 51 | this.on(); 52 | } else { 53 | this.off(); 54 | } 55 | }; 56 | 57 | player.on(player.exports.PlayerEvent.AirplayAvailable, airPlayAvailableHandler); 58 | player.on(player.exports.PlayerEvent.AirplayChanged, airPlayChangedHandler); 59 | 60 | // Startup init 61 | airPlayAvailableHandler(); // Hide button if AirPlay is not available 62 | airPlayChangedHandler(); 63 | } 64 | } -------------------------------------------------------------------------------- /src/ts/components/audiotracklistbox.ts: -------------------------------------------------------------------------------- 1 | import {ListBox} from './listbox'; 2 | import {UIInstanceManager} from '../uimanager'; 3 | import {AudioTrackSwitchHandler} from '../audiotrackutils'; 4 | import { PlayerAPI } from 'bitmovin-player'; 5 | 6 | /** 7 | * A element that is similar to a select box where the user can select a subtitle 8 | * 9 | * @category Components 10 | */ 11 | export class AudioTrackListBox extends ListBox { 12 | 13 | configure(player: PlayerAPI, uimanager: UIInstanceManager): void { 14 | super.configure(player, uimanager); 15 | new AudioTrackSwitchHandler(player, this, uimanager); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/ts/components/audiotrackselectbox.ts: -------------------------------------------------------------------------------- 1 | import {SelectBox} from './selectbox'; 2 | import {ListSelectorConfig} from './listselector'; 3 | import {UIInstanceManager} from '../uimanager'; 4 | import {AudioTrackSwitchHandler} from '../audiotrackutils'; 5 | import { PlayerAPI } from 'bitmovin-player'; 6 | 7 | /** 8 | * A select box providing a selection between available audio tracks (e.g. different languages). 9 | * 10 | * @category Components 11 | */ 12 | export class AudioTrackSelectBox extends SelectBox { 13 | 14 | constructor(config: ListSelectorConfig = {}) { 15 | super(config); 16 | 17 | this.config = this.mergeConfig(config, { 18 | cssClasses: ['ui-audiotrackselectbox'], 19 | }, this.config); 20 | } 21 | 22 | configure(player: PlayerAPI, uimanager: UIInstanceManager): void { 23 | super.configure(player, uimanager); 24 | 25 | new AudioTrackSwitchHandler(player, this, uimanager); 26 | } 27 | } -------------------------------------------------------------------------------- /src/ts/components/caststatusoverlay.ts: -------------------------------------------------------------------------------- 1 | import {ContainerConfig, Container} from './container'; 2 | import {Label, LabelConfig} from './label'; 3 | import {UIInstanceManager} from '../uimanager'; 4 | import { CastStartedEvent, CastWaitingForDeviceEvent, PlayerAPI } from 'bitmovin-player'; 5 | import { i18n } from '../localization/i18n'; 6 | 7 | /** 8 | * Overlays the player and displays the status of a Cast session. 9 | * 10 | * @category Components 11 | */ 12 | export class CastStatusOverlay extends Container { 13 | 14 | private statusLabel: Label; 15 | 16 | constructor(config: ContainerConfig = {}) { 17 | super(config); 18 | 19 | this.statusLabel = new Label({ cssClass: 'ui-cast-status-label' }); 20 | 21 | this.config = this.mergeConfig(config, { 22 | cssClass: 'ui-cast-status-overlay', 23 | components: [this.statusLabel], 24 | hidden: true, 25 | }, this.config); 26 | } 27 | 28 | configure(player: PlayerAPI, uimanager: UIInstanceManager): void { 29 | super.configure(player, uimanager); 30 | 31 | player.on(player.exports.PlayerEvent.CastWaitingForDevice, 32 | (event: CastWaitingForDeviceEvent) => { 33 | this.show(); 34 | // Get device name and update status text while connecting 35 | let castDeviceName = event.castPayload.deviceName; 36 | this.statusLabel.setText(i18n.getLocalizer('connectingTo', { castDeviceName })); 37 | }); 38 | player.on(player.exports.PlayerEvent.CastStarted, (event: CastStartedEvent) => { 39 | // Session is started or resumed 40 | // For cases when a session is resumed, we do not receive the previous events and therefore show the status panel 41 | // here too 42 | this.show(); 43 | let castDeviceName = event.deviceName; 44 | this.statusLabel.setText(i18n.getLocalizer('playingOn', { castDeviceName })); 45 | }); 46 | player.on(player.exports.PlayerEvent.CastStopped, (event) => { 47 | // Cast session gone, hide the status panel 48 | this.hide(); 49 | }); 50 | } 51 | } -------------------------------------------------------------------------------- /src/ts/components/casttogglebutton.ts: -------------------------------------------------------------------------------- 1 | import {ToggleButton, ToggleButtonConfig} from './togglebutton'; 2 | import {UIInstanceManager} from '../uimanager'; 3 | import { PlayerAPI } from 'bitmovin-player'; 4 | import { i18n } from '../localization/i18n'; 5 | 6 | /** 7 | * A button that toggles casting to a Cast receiver. 8 | * 9 | * @category Buttons 10 | */ 11 | export class CastToggleButton extends ToggleButton { 12 | 13 | constructor(config: ToggleButtonConfig = {}) { 14 | super(config); 15 | 16 | this.config = this.mergeConfig(config, { 17 | cssClass: 'ui-casttogglebutton', 18 | text: i18n.getLocalizer('googleCast'), 19 | }, this.config); 20 | } 21 | 22 | configure(player: PlayerAPI, uimanager: UIInstanceManager): void { 23 | super.configure(player, uimanager); 24 | 25 | this.onClick.subscribe(() => { 26 | if (player.isCastAvailable()) { 27 | if (player.isCasting()) { 28 | player.castStop(); 29 | } else { 30 | player.castVideo(); 31 | } 32 | } else { 33 | if (console) { 34 | console.log('Cast unavailable'); 35 | } 36 | } 37 | }); 38 | 39 | let castAvailableHander = () => { 40 | if (player.isCastAvailable()) { 41 | this.show(); 42 | } else { 43 | this.hide(); 44 | } 45 | }; 46 | 47 | player.on(player.exports.PlayerEvent.CastAvailable, castAvailableHander); 48 | 49 | // Toggle button 'on' state 50 | player.on(player.exports.PlayerEvent.CastWaitingForDevice, () => { 51 | this.on(); 52 | }); 53 | player.on(player.exports.PlayerEvent.CastStarted, () => { 54 | // When a session is resumed, there is no CastStart event, so we also need to toggle here for such cases 55 | this.on(); 56 | }); 57 | player.on(player.exports.PlayerEvent.CastStopped, () => { 58 | this.off(); 59 | }); 60 | 61 | // Startup init 62 | castAvailableHander(); // Hide button if Cast not available 63 | if (player.isCasting()) { 64 | this.on(); 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /src/ts/components/castuicontainer.ts: -------------------------------------------------------------------------------- 1 | import {UIContainer, UIContainerConfig} from './uicontainer'; 2 | import {UIInstanceManager} from '../uimanager'; 3 | import {Timeout} from '../timeout'; 4 | import { PlayerAPI } from 'bitmovin-player'; 5 | 6 | /** 7 | * The base container for Cast receivers that contains all of the UI and takes care that the UI is shown on 8 | * certain playback events. 9 | * 10 | * @category Containers 11 | */ 12 | export class CastUIContainer extends UIContainer { 13 | 14 | private castUiHideTimeout: Timeout; 15 | 16 | constructor(config: UIContainerConfig) { 17 | super(config); 18 | } 19 | 20 | configure(player: PlayerAPI, uimanager: UIInstanceManager): void { 21 | super.configure(player, uimanager); 22 | 23 | let config = this.getConfig(); 24 | 25 | /* 26 | * Show UI on Cast devices at certain playback events 27 | * 28 | * Since a Cast receiver does not have a direct HCI, we show the UI on certain playback events to give the user 29 | * a chance to see on the screen what's going on, e.g. on play/pause or a seek the UI is shown and the user can 30 | * see the current time and position on the seek bar. 31 | * The UI is shown permanently while playback is paused, otherwise hides automatically after the configured 32 | * hide delay time. 33 | */ 34 | 35 | let isUiShown = false; 36 | 37 | let hideUi = () => { 38 | uimanager.onControlsHide.dispatch(this); 39 | isUiShown = false; 40 | }; 41 | 42 | this.castUiHideTimeout = new Timeout(config.hideDelay, hideUi); 43 | 44 | let showUi = () => { 45 | if (!isUiShown) { 46 | uimanager.onControlsShow.dispatch(this); 47 | isUiShown = true; 48 | } 49 | }; 50 | 51 | let showUiPermanently = () => { 52 | showUi(); 53 | this.castUiHideTimeout.clear(); 54 | }; 55 | 56 | let showUiWithTimeout = () => { 57 | showUi(); 58 | this.castUiHideTimeout.start(); 59 | }; 60 | 61 | let showUiAfterSeek = () => { 62 | if (player.isPlaying()) { 63 | showUiWithTimeout(); 64 | } else { 65 | showUiPermanently(); 66 | } 67 | }; 68 | 69 | player.on(player.exports.PlayerEvent.Play, showUiWithTimeout); 70 | player.on(player.exports.PlayerEvent.Paused, showUiPermanently); 71 | player.on(player.exports.PlayerEvent.Seek, showUiPermanently); 72 | player.on(player.exports.PlayerEvent.Seeked, showUiAfterSeek); 73 | 74 | uimanager.getConfig().events.onUpdated.subscribe(showUiWithTimeout); 75 | } 76 | 77 | release(): void { 78 | super.release(); 79 | this.castUiHideTimeout.clear(); 80 | } 81 | } -------------------------------------------------------------------------------- /src/ts/components/clickoverlay.ts: -------------------------------------------------------------------------------- 1 | import {Button, ButtonConfig} from './button'; 2 | 3 | /** 4 | * Configuration interface for a {@link ClickOverlay}. 5 | * 6 | * @category Configs 7 | */ 8 | export interface ClickOverlayConfig extends ButtonConfig { 9 | /** 10 | * The url to open when the overlay is clicked. Set to null to disable the click handler. 11 | */ 12 | url?: string; 13 | } 14 | 15 | /** 16 | * A click overlay that opens an url in a new tab if clicked. 17 | * 18 | * @category Components 19 | */ 20 | export class ClickOverlay extends Button { 21 | 22 | constructor(config: ClickOverlayConfig = {}) { 23 | super(config); 24 | 25 | this.config = this.mergeConfig(config, { 26 | cssClass: 'ui-clickoverlay', 27 | role: this.config.role, 28 | }, this.config); 29 | } 30 | 31 | initialize(): void { 32 | super.initialize(); 33 | 34 | this.setUrl((this.config).url); 35 | let element = this.getDomElement(); 36 | element.on('click', () => { 37 | if (element.data('url')) { 38 | window.open(element.data('url'), '_blank'); 39 | } 40 | }); 41 | } 42 | 43 | /** 44 | * Gets the URL that should be followed when the watermark is clicked. 45 | * @returns {string} the watermark URL 46 | */ 47 | getUrl(): string { 48 | return this.getDomElement().data('url'); 49 | } 50 | 51 | setUrl(url: string): void { 52 | if (url === undefined || url == null) { 53 | url = ''; 54 | } 55 | this.getDomElement().data('url', url); 56 | } 57 | } -------------------------------------------------------------------------------- /src/ts/components/closebutton.ts: -------------------------------------------------------------------------------- 1 | import {ButtonConfig, Button} from './button'; 2 | import {UIInstanceManager} from '../uimanager'; 3 | import {Component, ComponentConfig} from './component'; 4 | import { PlayerAPI } from 'bitmovin-player'; 5 | import { i18n } from '../localization/i18n'; 6 | 7 | /** 8 | * Configuration interface for the {@link CloseButton}. 9 | * 10 | * @category Configs 11 | */ 12 | export interface CloseButtonConfig extends ButtonConfig { 13 | /** 14 | * The component that should be closed when the button is clicked. 15 | */ 16 | target: Component; 17 | } 18 | 19 | /** 20 | * A button that closes (hides) a configured component. 21 | * 22 | * @category Buttons 23 | */ 24 | export class CloseButton extends Button { 25 | 26 | constructor(config: CloseButtonConfig) { 27 | super(config); 28 | 29 | this.config = this.mergeConfig(config, { 30 | cssClass: 'ui-closebutton', 31 | text: i18n.getLocalizer('close'), 32 | } as CloseButtonConfig, this.config); 33 | } 34 | 35 | configure(player: PlayerAPI, uimanager: UIInstanceManager): void { 36 | super.configure(player, uimanager); 37 | 38 | let config = this.getConfig(); 39 | 40 | this.onClick.subscribe(() => { 41 | config.target.hide(); 42 | }); 43 | } 44 | } -------------------------------------------------------------------------------- /src/ts/components/fullscreentogglebutton.ts: -------------------------------------------------------------------------------- 1 | import { ToggleButton, ToggleButtonConfig } from './togglebutton'; 2 | import { UIInstanceManager } from '../uimanager'; 3 | import { PlayerAPI } from 'bitmovin-player'; 4 | import { i18n } from '../localization/i18n'; 5 | 6 | /** 7 | * A button that toggles the player between windowed and fullscreen view. 8 | * 9 | * @category Buttons 10 | */ 11 | export class FullscreenToggleButton extends ToggleButton { 12 | 13 | constructor(config: ToggleButtonConfig = {}) { 14 | super(config); 15 | 16 | this.config = this.mergeConfig(config, { 17 | cssClass: 'ui-fullscreentogglebutton', 18 | text: i18n.getLocalizer('fullscreen'), 19 | }, this.config); 20 | } 21 | 22 | configure(player: PlayerAPI, uimanager: UIInstanceManager): void { 23 | super.configure(player, uimanager); 24 | 25 | const isFullScreenAvailable = () => { 26 | return player.isViewModeAvailable(player.exports.ViewMode.Fullscreen); 27 | }; 28 | 29 | const fullscreenStateHandler = () => { 30 | player.getViewMode() === player.exports.ViewMode.Fullscreen ? this.on() : this.off(); 31 | }; 32 | 33 | const fullscreenAvailabilityChangedHandler = () => { 34 | isFullScreenAvailable() ? this.show() : this.hide(); 35 | }; 36 | 37 | player.on(player.exports.PlayerEvent.ViewModeChanged, fullscreenStateHandler); 38 | 39 | // Available only in our native SDKs for now 40 | if ((player.exports.PlayerEvent as any).ViewModeAvailabilityChanged) { 41 | player.on( 42 | (player.exports.PlayerEvent as any).ViewModeAvailabilityChanged, 43 | fullscreenAvailabilityChangedHandler, 44 | ); 45 | } 46 | 47 | uimanager.getConfig().events.onUpdated.subscribe(fullscreenAvailabilityChangedHandler); 48 | 49 | this.onClick.subscribe(() => { 50 | if (!isFullScreenAvailable()) { 51 | if (console) { 52 | console.log('Fullscreen unavailable'); 53 | } 54 | return; 55 | } 56 | 57 | const targetViewMode = 58 | player.getViewMode() === player.exports.ViewMode.Fullscreen 59 | ? player.exports.ViewMode.Inline 60 | : player.exports.ViewMode.Fullscreen; 61 | 62 | player.setViewMode(targetViewMode); 63 | }); 64 | 65 | // Startup init 66 | fullscreenAvailabilityChangedHandler(); 67 | fullscreenStateHandler(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/ts/components/hugereplaybutton.ts: -------------------------------------------------------------------------------- 1 | import {ButtonConfig, Button} from './button'; 2 | import {DOM} from '../dom'; 3 | import {UIInstanceManager} from '../uimanager'; 4 | import { PlayerAPI } from 'bitmovin-player'; 5 | import { i18n } from '../localization/i18n'; 6 | 7 | /** 8 | * A button to play/replay a video. 9 | * 10 | * @category Buttons 11 | */ 12 | export class HugeReplayButton extends Button { 13 | 14 | constructor(config: ButtonConfig = {}) { 15 | super(config); 16 | 17 | this.config = this.mergeConfig(config, { 18 | cssClass: 'ui-hugereplaybutton', 19 | text: i18n.getLocalizer('replay'), 20 | }, this.config); 21 | } 22 | 23 | configure(player: PlayerAPI, uimanager: UIInstanceManager): void { 24 | super.configure(player, uimanager); 25 | 26 | this.onClick.subscribe(() => { 27 | player.play('ui'); 28 | }); 29 | } 30 | 31 | protected toDomElement(): DOM { 32 | let buttonElement = super.toDomElement(); 33 | 34 | // Add child that contains the play button image 35 | // Setting the image directly on the button does not work together with scaling animations, because the button 36 | // can cover the whole video player are and scaling would extend it beyond. By adding an inner element, confined 37 | // to the size if the image, it can scale inside the player without overshooting. 38 | buttonElement.append(new DOM('div', { 39 | 'class': this.prefixCss('image'), 40 | })); 41 | 42 | return buttonElement; 43 | } 44 | } -------------------------------------------------------------------------------- /src/ts/components/metadatalabel.ts: -------------------------------------------------------------------------------- 1 | import {LabelConfig, Label} from './label'; 2 | import {UIInstanceManager} from '../uimanager'; 3 | import { PlayerAPI } from 'bitmovin-player'; 4 | 5 | /** 6 | * Enumerates the types of content that the {@link MetadataLabel} can display. 7 | */ 8 | export enum MetadataLabelContent { 9 | /** 10 | * Title of the data source. 11 | */ 12 | Title, 13 | /** 14 | * Description fo the data source. 15 | */ 16 | Description, 17 | } 18 | 19 | /** 20 | * Configuration interface for {@link MetadataLabel}. 21 | * 22 | * @category Configs 23 | */ 24 | export interface MetadataLabelConfig extends LabelConfig { 25 | /** 26 | * The type of content that should be displayed in the label. 27 | */ 28 | content: MetadataLabelContent; 29 | } 30 | 31 | /** 32 | * A label that can be configured to display certain metadata. 33 | * 34 | * @category Labels 35 | */ 36 | export class MetadataLabel extends Label { 37 | 38 | constructor(config: MetadataLabelConfig) { 39 | super(config); 40 | 41 | this.config = this.mergeConfig(config, { 42 | cssClasses: ['label-metadata', 'label-metadata-' + MetadataLabelContent[config.content].toLowerCase()], 43 | } as MetadataLabelConfig, this.config); 44 | } 45 | 46 | configure(player: PlayerAPI, uimanager: UIInstanceManager): void { 47 | super.configure(player, uimanager); 48 | 49 | let config = this.getConfig(); 50 | let uiconfig = uimanager.getConfig(); 51 | 52 | let init = () => { 53 | switch (config.content) { 54 | case MetadataLabelContent.Title: 55 | this.setText(uiconfig.metadata.title); 56 | break; 57 | case MetadataLabelContent.Description: 58 | this.setText(uiconfig.metadata.description); 59 | break; 60 | } 61 | }; 62 | 63 | let unload = () => { 64 | this.setText(null); 65 | }; 66 | 67 | // Init label 68 | init(); 69 | // Clear labels when source is unloaded 70 | player.on(player.exports.PlayerEvent.SourceUnloaded, unload); 71 | 72 | uimanager.getConfig().events.onUpdated.subscribe(init); 73 | } 74 | } -------------------------------------------------------------------------------- /src/ts/components/pictureinpicturetogglebutton.ts: -------------------------------------------------------------------------------- 1 | import {ToggleButton, ToggleButtonConfig} from './togglebutton'; 2 | import {UIInstanceManager} from '../uimanager'; 3 | import { PlayerAPI } from 'bitmovin-player'; 4 | import { i18n } from '../localization/i18n'; 5 | 6 | /** 7 | * A button that toggles Apple macOS picture-in-picture mode. 8 | * 9 | * @category Buttons 10 | */ 11 | export class PictureInPictureToggleButton extends ToggleButton { 12 | 13 | constructor(config: ToggleButtonConfig = {}) { 14 | super(config); 15 | 16 | this.config = this.mergeConfig(config, { 17 | cssClass: 'ui-piptogglebutton', 18 | text: i18n.getLocalizer('pictureInPicture'), 19 | }, this.config); 20 | } 21 | 22 | configure(player: PlayerAPI, uimanager: UIInstanceManager): void { 23 | super.configure(player, uimanager); 24 | 25 | const isPictureInPictureAvailable = () => { 26 | return player.isViewModeAvailable(player.exports.ViewMode.PictureInPicture); 27 | }; 28 | 29 | const pictureInPictureStateHandler = () => { 30 | player.getViewMode() === player.exports.ViewMode.PictureInPicture ? this.on() : this.off(); 31 | }; 32 | 33 | const pictureInPictureAvailabilityChangedHandler = () => { 34 | isPictureInPictureAvailable() ? this.show() : this.hide(); 35 | }; 36 | 37 | player.on(player.exports.PlayerEvent.ViewModeChanged, pictureInPictureStateHandler); 38 | 39 | // Available only in our native SDKs for now 40 | if ((player.exports.PlayerEvent as any).ViewModeAvailabilityChanged) { 41 | player.on( 42 | (player.exports.PlayerEvent as any).ViewModeAvailabilityChanged, 43 | pictureInPictureAvailabilityChangedHandler, 44 | ); 45 | } 46 | 47 | uimanager.getConfig().events.onUpdated.subscribe(pictureInPictureAvailabilityChangedHandler); 48 | 49 | this.onClick.subscribe(() => { 50 | if (!isPictureInPictureAvailable()) { 51 | if (console) { 52 | console.log('PIP unavailable'); 53 | } 54 | return; 55 | } 56 | 57 | const targetViewMode = 58 | player.getViewMode() === player.exports.ViewMode.PictureInPicture 59 | ? player.exports.ViewMode.Inline 60 | : player.exports.ViewMode.PictureInPicture; 61 | 62 | player.setViewMode(targetViewMode); 63 | }); 64 | 65 | // Startup init 66 | pictureInPictureAvailabilityChangedHandler(); // Hide button if PIP not available 67 | pictureInPictureStateHandler(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/ts/components/playbackspeedselectbox.ts: -------------------------------------------------------------------------------- 1 | import {SelectBox} from './selectbox'; 2 | import {ListSelectorConfig} from './listselector'; 3 | import {UIInstanceManager} from '../uimanager'; 4 | import { PlayerAPI } from 'bitmovin-player'; 5 | import { i18n } from '../localization/i18n'; 6 | 7 | /** 8 | * A select box providing a selection of different playback speeds. 9 | * 10 | * @category Components 11 | */ 12 | export class PlaybackSpeedSelectBox extends SelectBox { 13 | protected defaultPlaybackSpeeds: number[]; 14 | 15 | constructor(config: ListSelectorConfig = {}) { 16 | super(config); 17 | this.defaultPlaybackSpeeds = [0.25, 0.5, 1, 1.5, 2]; 18 | 19 | this.config = this.mergeConfig(config, { 20 | cssClasses: ['ui-playbackspeedselectbox'], 21 | }, this.config); 22 | } 23 | 24 | configure(player: PlayerAPI, uimanager: UIInstanceManager): void { 25 | super.configure(player, uimanager); 26 | 27 | this.addDefaultItems(); 28 | 29 | this.onItemSelected.subscribe((sender: PlaybackSpeedSelectBox, value: string) => { 30 | player.setPlaybackSpeed(parseFloat(value)); 31 | this.selectItem(value); 32 | }); 33 | 34 | const setDefaultValue = (): void => { 35 | const playbackSpeed = player.getPlaybackSpeed(); 36 | this.setSpeed(playbackSpeed); 37 | }; 38 | 39 | player.on(player.exports.PlayerEvent.PlaybackSpeedChanged, setDefaultValue); 40 | uimanager.getConfig().events.onUpdated.subscribe(setDefaultValue); 41 | } 42 | 43 | setSpeed(speed: number): void { 44 | if (!this.selectItem(String(speed))) { 45 | // a playback speed was set which is not in the list, add it to the list to show it to the user 46 | this.clearItems(); 47 | this.addDefaultItems([speed]); 48 | this.selectItem(String(speed)); 49 | } 50 | } 51 | 52 | addDefaultItems(customItems: number[] = []): void { 53 | const sortedSpeeds = this.defaultPlaybackSpeeds.concat(customItems).sort(); 54 | 55 | sortedSpeeds.forEach(element => { 56 | if (element !== 1) { 57 | this.addItem(String(element), `${element}x`); 58 | } else { 59 | this.addItem(String(element), i18n.getLocalizer('normal')); 60 | } 61 | }); 62 | } 63 | 64 | clearItems(): void { 65 | this.items = []; 66 | this.selectedItem = null; 67 | } 68 | } -------------------------------------------------------------------------------- /src/ts/components/playbacktoggleoverlay.ts: -------------------------------------------------------------------------------- 1 | import {Container, ContainerConfig} from './container'; 2 | import {HugePlaybackToggleButton} from './hugeplaybacktogglebutton'; 3 | 4 | /** 5 | * @category Configs 6 | */ 7 | export interface PlaybackToggleOverlayConfig extends ContainerConfig { 8 | /** 9 | * Specify whether the player should be set to enter fullscreen by clicking on the playback toggle button 10 | * when initiating the initial playback. 11 | * Default is false. 12 | */ 13 | enterFullscreenOnInitialPlayback?: boolean; 14 | } 15 | 16 | /** 17 | * Overlays the player and displays error messages. 18 | * 19 | * @category Components 20 | */ 21 | export class PlaybackToggleOverlay extends Container { 22 | 23 | private playbackToggleButton: HugePlaybackToggleButton; 24 | 25 | constructor(config: PlaybackToggleOverlayConfig = {}) { 26 | super(config); 27 | 28 | this.playbackToggleButton = new HugePlaybackToggleButton({ 29 | enterFullscreenOnInitialPlayback: Boolean(config.enterFullscreenOnInitialPlayback), 30 | }); 31 | 32 | this.config = this.mergeConfig(config, { 33 | cssClass: 'ui-playbacktoggle-overlay', 34 | components: [this.playbackToggleButton], 35 | }, this.config); 36 | } 37 | } -------------------------------------------------------------------------------- /src/ts/components/replaybutton.ts: -------------------------------------------------------------------------------- 1 | import { ButtonConfig, Button } from './button'; 2 | import { UIInstanceManager } from '../uimanager'; 3 | import { PlayerAPI } from 'bitmovin-player'; 4 | import { i18n } from '../localization/i18n'; 5 | import { PlayerUtils } from '../playerutils'; 6 | import LiveStreamDetectorEventArgs = PlayerUtils.LiveStreamDetectorEventArgs; 7 | 8 | /** 9 | * A button to play/replay a video. 10 | * 11 | * @category Buttons 12 | */ 13 | export class ReplayButton extends Button { 14 | 15 | constructor(config: ButtonConfig = {}) { 16 | super(config); 17 | 18 | this.config = this.mergeConfig(config, { 19 | cssClass: 'ui-replaybutton', 20 | text: i18n.getLocalizer('replay'), 21 | ariaLabel: i18n.getLocalizer('replay'), 22 | }, this.config); 23 | } 24 | 25 | configure(player: PlayerAPI, uimanager: UIInstanceManager): void { 26 | super.configure(player, uimanager); 27 | 28 | if (player.isLive()) { 29 | this.hide(); 30 | } 31 | 32 | const liveStreamDetector = new PlayerUtils.LiveStreamDetector(player, uimanager); 33 | liveStreamDetector.onLiveChanged.subscribe((sender, args: LiveStreamDetectorEventArgs) => { 34 | if (args.live) { 35 | this.hide(); 36 | } else { 37 | this.show(); 38 | } 39 | }); 40 | 41 | this.onClick.subscribe(() => { 42 | if (!player.hasEnded()) { 43 | player.seek(0); 44 | // Not calling `play` will keep the play/pause state as is 45 | } else { 46 | // If playback has already ended, calling `play` will automatically restart from the beginning 47 | player.play('ui'); 48 | } 49 | }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/ts/components/seekbarbufferlevel.ts: -------------------------------------------------------------------------------- 1 | import {PlayerAPI} from 'bitmovin-player'; 2 | 3 | export function getMinBufferLevel(player: PlayerAPI): number { 4 | 5 | const playerDuration = player.getDuration(); 6 | 7 | const videoBufferLength = player.getVideoBufferLength(); 8 | const audioBufferLength = player.getAudioBufferLength(); 9 | // Calculate the buffer length which is the smaller length of the audio and video buffers. If one of these 10 | // buffers is not available, we set it's value to MAX_VALUE to make sure that the other real value is taken 11 | // as the buffer length. 12 | let bufferLength = Math.min( 13 | videoBufferLength != null ? videoBufferLength : Number.MAX_VALUE, 14 | audioBufferLength != null ? audioBufferLength : Number.MAX_VALUE); 15 | // If both buffer lengths are missing, we set the buffer length to zero 16 | if (bufferLength === Number.MAX_VALUE) { 17 | bufferLength = 0; 18 | } 19 | 20 | return 100 / playerDuration * bufferLength; 21 | } 22 | -------------------------------------------------------------------------------- /src/ts/components/settingspanelpagebackbutton.ts: -------------------------------------------------------------------------------- 1 | import {UIInstanceManager} from '../uimanager'; 2 | import {SettingsPanelPageNavigatorButton, SettingsPanelPageNavigatorConfig} from './settingspanelpagenavigatorbutton'; 3 | import { PlayerAPI } from 'bitmovin-player'; 4 | 5 | /** 6 | * @category Buttons 7 | */ 8 | export class SettingsPanelPageBackButton extends SettingsPanelPageNavigatorButton { 9 | 10 | constructor(config: SettingsPanelPageNavigatorConfig) { 11 | super(config); 12 | 13 | this.config = this.mergeConfig(config, { 14 | cssClass: 'ui-settingspanelpagebackbutton', 15 | text: 'back', 16 | } as SettingsPanelPageNavigatorConfig, this.config); 17 | } 18 | 19 | configure(player: PlayerAPI, uimanager: UIInstanceManager): void { 20 | super.configure(player, uimanager); 21 | 22 | this.onClick.subscribe(() => { 23 | this.popPage(); 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/ts/components/settingspanelpagenavigatorbutton.ts: -------------------------------------------------------------------------------- 1 | import {Button, ButtonConfig} from './button'; 2 | import {SettingsPanel} from './settingspanel'; 3 | import {SettingsPanelPage} from './settingspanelpage'; 4 | import { PlayerAPI } from 'bitmovin-player'; 5 | import { UIInstanceManager } from '../uimanager'; 6 | 7 | /** 8 | * Configuration interface for a {@link SettingsPanelPageNavigatorButton} 9 | * 10 | * @category Configs 11 | */ 12 | export interface SettingsPanelPageNavigatorConfig extends ButtonConfig { 13 | /** 14 | * Container `SettingsPanel` where the navigation takes place 15 | */ 16 | container: SettingsPanel; 17 | /** 18 | * Page where the button should navigate to 19 | * If empty it will navigate to the root page (not intended to use as navigate back behavior) 20 | */ 21 | targetPage?: SettingsPanelPage; 22 | 23 | /** 24 | * WCAG20 standard: Establishes relationships between objects and their label(s) 25 | */ 26 | ariaLabelledBy?: string; 27 | } 28 | 29 | /** 30 | * Can be used to navigate between SettingsPanelPages 31 | * 32 | * Example: 33 | * let settingPanelNavigationButton = new SettingsPanelPageNavigatorButton({ 34 | * container: settingsPanel, 35 | * targetPage: settingsPanelPage, 36 | * }); 37 | * 38 | * settingsPanelPage.addComponent(settingPanelNavigationButton); 39 | * 40 | * Don't forget to add the settingPanelNavigationButton to the settingsPanelPage. 41 | * 42 | * @category Buttons 43 | */ 44 | export class SettingsPanelPageNavigatorButton extends Button { 45 | private readonly container: SettingsPanel; 46 | private readonly targetPage?: SettingsPanelPage; 47 | 48 | constructor(config: SettingsPanelPageNavigatorConfig) { 49 | super(config); 50 | this.config = this.mergeConfig(config, {} as SettingsPanelPageNavigatorConfig, this.config); 51 | 52 | this.container = (this.config as SettingsPanelPageNavigatorConfig).container; 53 | this.targetPage = (this.config as SettingsPanelPageNavigatorConfig).targetPage; 54 | } 55 | 56 | /** 57 | * navigate one level back 58 | */ 59 | popPage() { 60 | this.container.popSettingsPanelPage(); 61 | } 62 | 63 | /** 64 | * navigate to the target page 65 | */ 66 | pushTargetPage() { 67 | this.container.setActivePage(this.targetPage); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/ts/components/settingspanelpageopenbutton.ts: -------------------------------------------------------------------------------- 1 | import {UIInstanceManager} from '../uimanager'; 2 | import {SettingsPanelPageNavigatorButton, SettingsPanelPageNavigatorConfig} from './settingspanelpagenavigatorbutton'; 3 | import { PlayerAPI } from 'bitmovin-player'; 4 | import { i18n } from '../localization/i18n'; 5 | 6 | /** 7 | * @category Buttons 8 | */ 9 | export class SettingsPanelPageOpenButton extends SettingsPanelPageNavigatorButton { 10 | constructor(config: SettingsPanelPageNavigatorConfig) { 11 | super(config); 12 | 13 | this.config = this.mergeConfig(config, { 14 | cssClass: 'ui-settingspanelpageopenbutton', 15 | text: i18n.getLocalizer('open'), 16 | role: 'menuitem', 17 | } as SettingsPanelPageNavigatorConfig, this.config); 18 | } 19 | 20 | configure(player: PlayerAPI, uimanager: UIInstanceManager): void { 21 | super.configure(player, uimanager); 22 | 23 | this.getDomElement().attr('aria-haspopup', 'true'); 24 | this.getDomElement().attr('aria-owns', this.config.targetPage.getConfig().id); 25 | 26 | this.onClick.subscribe(() => { 27 | this.pushTargetPage(); 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/ts/components/spacer.ts: -------------------------------------------------------------------------------- 1 | import {Component, ComponentConfig} from './component'; 2 | 3 | /** 4 | * A dummy component that just reserves some space and does nothing else. 5 | * 6 | * @category Components 7 | */ 8 | export class Spacer extends Component { 9 | 10 | constructor(config: ComponentConfig = {}) { 11 | super(config); 12 | 13 | this.config = this.mergeConfig(config, { 14 | cssClass: 'ui-spacer', 15 | }, this.config); 16 | } 17 | 18 | 19 | protected onShowEvent(): void { 20 | // disable event firing by overwriting and not calling super 21 | } 22 | 23 | protected onHideEvent(): void { 24 | // disable event firing by overwriting and not calling super 25 | } 26 | 27 | protected onHoverChangedEvent(hovered: boolean): void { 28 | // disable event firing by overwriting and not calling super 29 | } 30 | } -------------------------------------------------------------------------------- /src/ts/components/subtitlelistbox.ts: -------------------------------------------------------------------------------- 1 | import {ListBox} from './listbox'; 2 | import {UIInstanceManager} from '../uimanager'; 3 | import {SubtitleSwitchHandler} from '../subtitleutils'; 4 | import { PlayerAPI } from 'bitmovin-player'; 5 | 6 | /** 7 | * A element that is similar to a select box where the user can select a subtitle 8 | * 9 | * @category Components 10 | */ 11 | export class SubtitleListBox extends ListBox { 12 | 13 | configure(player: PlayerAPI, uimanager: UIInstanceManager): void { 14 | super.configure(player, uimanager); 15 | 16 | new SubtitleSwitchHandler(player, this, uimanager); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/ts/components/subtitleselectbox.ts: -------------------------------------------------------------------------------- 1 | import {SelectBox} from './selectbox'; 2 | import {ListSelectorConfig} from './listselector'; 3 | import {UIInstanceManager} from '../uimanager'; 4 | import {SubtitleSwitchHandler} from '../subtitleutils'; 5 | import { PlayerAPI } from 'bitmovin-player'; 6 | import { i18n } from '../localization/i18n'; 7 | 8 | /** 9 | * A select box providing a selection between available subtitle and caption tracks. 10 | * 11 | * @category Components 12 | */ 13 | export class SubtitleSelectBox extends SelectBox { 14 | 15 | constructor(config: ListSelectorConfig = {}) { 16 | super(config); 17 | 18 | this.config = this.mergeConfig(config, { 19 | cssClasses: ['ui-subtitleselectbox'], 20 | ariaLabel: i18n.getLocalizer('subtitle.select'), 21 | }, this.config); 22 | } 23 | 24 | configure(player: PlayerAPI, uimanager: UIInstanceManager): void { 25 | super.configure(player, uimanager); 26 | 27 | new SubtitleSwitchHandler(player, this, uimanager); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/ts/components/subtitlesettings/backgroundopacityselectbox.ts: -------------------------------------------------------------------------------- 1 | import { SubtitleSettingSelectBox, SubtitleSettingSelectBoxConfig } from './subtitlesettingselectbox'; 2 | import {UIInstanceManager} from '../../uimanager'; 3 | import { PlayerAPI } from 'bitmovin-player'; 4 | import { i18n } from '../../localization/i18n'; 5 | 6 | /** 7 | * A select box providing a selection of different background opacity. 8 | * 9 | * @category Components 10 | */ 11 | export class BackgroundOpacitySelectBox extends SubtitleSettingSelectBox { 12 | 13 | constructor(config: SubtitleSettingSelectBoxConfig) { 14 | super(config); 15 | 16 | this.config = this.mergeConfig(config, { 17 | cssClasses: ['ui-subtitlesettingsbackgroundopacityselectbox'], 18 | }, this.config); 19 | } 20 | 21 | configure(player: PlayerAPI, uimanager: UIInstanceManager): void { 22 | super.configure(player, uimanager); 23 | 24 | this.addItem(null, i18n.getLocalizer('default')); 25 | this.addItem('100', i18n.getLocalizer('percent', { value: 100 })); 26 | this.addItem('75', i18n.getLocalizer('percent', { value: 75 })); 27 | this.addItem('50', i18n.getLocalizer('percent', { value: 50 })); 28 | this.addItem('25', i18n.getLocalizer('percent', { value: 25 })); 29 | this.addItem('0', i18n.getLocalizer('percent', { value: 0 })); 30 | 31 | this.onItemSelected.subscribe((sender, key: string) => { 32 | this.settingsManager.backgroundOpacity.value = key; 33 | 34 | // Color and opacity go together, so we need to... 35 | if (!this.settingsManager.backgroundOpacity.isSet()) { 36 | // ... clear the color when the opacity is not set 37 | this.settingsManager.backgroundColor.clear(); 38 | } else if (!this.settingsManager.backgroundColor.isSet()) { 39 | // ... set a color when the opacity is set 40 | this.settingsManager.backgroundColor.value = 'black'; 41 | } 42 | }); 43 | 44 | // Update selected item when value is set from somewhere else 45 | this.settingsManager.backgroundOpacity.onChanged.subscribe((sender, property) => { 46 | this.selectItem(property.value); 47 | }); 48 | 49 | // Load initial value 50 | if (this.settingsManager.backgroundOpacity.isSet()) { 51 | this.selectItem(this.settingsManager.backgroundOpacity.value); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/ts/components/subtitlesettings/characteredgecolorselectbox.ts: -------------------------------------------------------------------------------- 1 | import { SubtitleSettingSelectBox, SubtitleSettingSelectBoxConfig } from './subtitlesettingselectbox'; 2 | import { UIInstanceManager } from '../../uimanager'; 3 | import { PlayerAPI } from 'bitmovin-player'; 4 | import { i18n } from '../../localization/i18n'; 5 | 6 | /** 7 | * A select box providing a selection of different character edge colors. 8 | * 9 | * @category Components 10 | */ 11 | export class CharacterEdgeColorSelectBox extends SubtitleSettingSelectBox { 12 | 13 | constructor(config: SubtitleSettingSelectBoxConfig) { 14 | super(config); 15 | 16 | this.config = this.mergeConfig(config, { 17 | cssClasses: ['ui-subtitle-settings-character-edge-color-select-box'], 18 | }, this.config); 19 | } 20 | 21 | configure(player: PlayerAPI, uimanager: UIInstanceManager): void { 22 | super.configure(player, uimanager); 23 | 24 | this.addItem(null, i18n.getLocalizer('default')); 25 | this.addItem('white', i18n.getLocalizer('colors.white')); 26 | this.addItem('black', i18n.getLocalizer('colors.black')); 27 | this.addItem('red', i18n.getLocalizer('colors.red')); 28 | this.addItem('green', i18n.getLocalizer('colors.green')); 29 | this.addItem('blue', i18n.getLocalizer('colors.blue')); 30 | this.addItem('cyan', i18n.getLocalizer('colors.cyan')); 31 | this.addItem('yellow', i18n.getLocalizer('colors.yellow')); 32 | this.addItem('magenta', i18n.getLocalizer('colors.magenta')); 33 | 34 | this.onItemSelected.subscribe((sender, key: string) => { 35 | this.settingsManager.characterEdgeColor.value = key; 36 | 37 | // Edge type and color go together, so we need to... 38 | if (!this.settingsManager.characterEdgeColor.isSet()) { 39 | // ... clear the edge type when the color is not set 40 | this.settingsManager.characterEdge.clear(); 41 | } else if (!this.settingsManager.characterEdge.isSet()) { 42 | // ... set a edge type when the color is set 43 | this.settingsManager.characterEdge.value = 'uniform'; 44 | } 45 | }); 46 | 47 | // Update selected item when value is set from somewhere else 48 | this.settingsManager.characterEdgeColor.onChanged.subscribe((sender, property) => { 49 | this.selectItem(property.value); 50 | }); 51 | 52 | // Load initial value 53 | if (this.settingsManager.characterEdgeColor.isSet()) { 54 | this.selectItem(this.settingsManager.characterEdgeColor.value); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/ts/components/subtitlesettings/fontfamilyselectbox.ts: -------------------------------------------------------------------------------- 1 | import { SubtitleSettingSelectBox, SubtitleSettingSelectBoxConfig } from './subtitlesettingselectbox'; 2 | import {UIInstanceManager} from '../../uimanager'; 3 | import { PlayerAPI } from 'bitmovin-player'; 4 | import { i18n } from '../../localization/i18n'; 5 | 6 | /** 7 | * A select box providing a selection of different font family. 8 | * 9 | * @category Components 10 | */ 11 | export class FontFamilySelectBox extends SubtitleSettingSelectBox { 12 | 13 | constructor(config: SubtitleSettingSelectBoxConfig) { 14 | super(config); 15 | 16 | this.config = this.mergeConfig(config, { 17 | cssClasses: ['ui-subtitlesettingsfontfamilyselectbox'], 18 | }, this.config); 19 | } 20 | 21 | configure(player: PlayerAPI, uimanager: UIInstanceManager): void { 22 | super.configure(player, uimanager); 23 | 24 | this.addItem(null, i18n.getLocalizer('default')); 25 | this.addItem('monospacedserif', i18n.getLocalizer('settings.subtitles.font.family.monospacedserif')); 26 | this.addItem('proportionalserif', i18n.getLocalizer('settings.subtitles.font.family.proportionalserif')); 27 | this.addItem('monospacedsansserif', i18n.getLocalizer('settings.subtitles.font.family.monospacedsansserif')); 28 | this.addItem('proportionalsansserif', i18n.getLocalizer('settings.subtitles.font.family.proportionalsansserif')); 29 | this.addItem('casual', i18n.getLocalizer('settings.subtitles.font.family.casual')); 30 | this.addItem('cursive', i18n.getLocalizer('settings.subtitles.font.family.cursive')); 31 | this.addItem('smallcapital', i18n.getLocalizer('settings.subtitles.font.family.smallcapital')); 32 | 33 | this.settingsManager.fontFamily.onChanged.subscribe((sender, property) => { 34 | if (property.isSet()) { 35 | this.toggleOverlayClass('fontfamily-' + property.value); 36 | } else { 37 | this.toggleOverlayClass(null); 38 | } 39 | 40 | // Select the item in case the property was set from outside 41 | this.selectItem(property.value); 42 | }); 43 | 44 | this.onItemSelected.subscribe((sender, key: string) => { 45 | this.settingsManager.fontFamily.value = key; 46 | }); 47 | 48 | // Load initial value 49 | if (this.settingsManager.fontFamily.isSet()) { 50 | this.selectItem(this.settingsManager.fontFamily.value); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/ts/components/subtitlesettings/fontopacityselectbox.ts: -------------------------------------------------------------------------------- 1 | import { SubtitleSettingSelectBox, SubtitleSettingSelectBoxConfig } from './subtitlesettingselectbox'; 2 | import {UIInstanceManager} from '../../uimanager'; 3 | import { PlayerAPI } from 'bitmovin-player'; 4 | import { i18n } from '../../localization/i18n'; 5 | 6 | /** 7 | * A select box providing a selection of different font colors. 8 | * 9 | * @category Components 10 | */ 11 | export class FontOpacitySelectBox extends SubtitleSettingSelectBox { 12 | 13 | constructor(config: SubtitleSettingSelectBoxConfig) { 14 | super(config); 15 | 16 | this.config = this.mergeConfig(config, { 17 | cssClasses: ['ui-subtitlesettingsfontopacityselectbox'], 18 | }, this.config); 19 | } 20 | 21 | configure(player: PlayerAPI, uimanager: UIInstanceManager): void { 22 | super.configure(player, uimanager); 23 | 24 | this.addItem(null, i18n.getLocalizer('default')); 25 | this.addItem('100', i18n.getLocalizer('percent', { value: 100 })); 26 | this.addItem('75', i18n.getLocalizer('percent', { value: 75 })); 27 | this.addItem('50', i18n.getLocalizer('percent', { value: 50 })); 28 | this.addItem('25', i18n.getLocalizer('percent', { value: 25 })); 29 | 30 | this.onItemSelected.subscribe((sender, key: string) => { 31 | this.settingsManager.fontOpacity.value = key; 32 | 33 | // Color and opacity go together, so we need to... 34 | if (!this.settingsManager.fontOpacity.isSet()) { 35 | // ... clear the color when the opacity is not set 36 | this.settingsManager.fontColor.clear(); 37 | } else if (!this.settingsManager.fontColor.isSet()) { 38 | // ... set a color when the opacity is set 39 | this.settingsManager.fontColor.value = 'white'; 40 | } 41 | }); 42 | 43 | // Update selected item when value is set from somewhere else 44 | this.settingsManager.fontOpacity.onChanged.subscribe((sender, property) => { 45 | this.selectItem(property.value); 46 | }); 47 | 48 | // Load initial value 49 | if (this.settingsManager.fontOpacity.isSet()) { 50 | this.selectItem(this.settingsManager.fontOpacity.value); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/ts/components/subtitlesettings/fontstyleselectbox.ts: -------------------------------------------------------------------------------- 1 | import { SubtitleSettingSelectBox, SubtitleSettingSelectBoxConfig } from './subtitlesettingselectbox'; 2 | import { UIInstanceManager } from '../../uimanager'; 3 | import { PlayerAPI } from 'bitmovin-player'; 4 | import { i18n } from '../../localization/i18n'; 5 | 6 | /** 7 | * A select box providing a selection of different font styles. 8 | * 9 | * @category Components 10 | */ 11 | export class FontStyleSelectBox extends SubtitleSettingSelectBox { 12 | 13 | constructor(config: SubtitleSettingSelectBoxConfig) { 14 | super(config); 15 | 16 | this.config = this.mergeConfig(config, { 17 | cssClasses: ['ui-subtitle-settings-font-style-select-box'], 18 | }, this.config); 19 | } 20 | 21 | configure(player: PlayerAPI, uimanager: UIInstanceManager): void { 22 | super.configure(player, uimanager); 23 | 24 | this.addItem(null, i18n.getLocalizer('default')); 25 | this.addItem('italic', i18n.getLocalizer('settings.subtitles.font.style.italic')); 26 | this.addItem('bold', i18n.getLocalizer('settings.subtitles.font.style.bold')); 27 | 28 | this.settingsManager?.fontStyle.onChanged.subscribe((sender, property) => { 29 | if (property.isSet()) { 30 | this.toggleOverlayClass('fontstyle-' + property.value); 31 | } else { 32 | this.toggleOverlayClass(null); 33 | } 34 | 35 | // Select the item in case the property was set from outside 36 | this.selectItem(property.value); 37 | }); 38 | 39 | this.onItemSelected.subscribe((sender, key: string) => { 40 | if (this.settingsManager) { 41 | this.settingsManager.fontStyle.value = key; 42 | } 43 | }); 44 | 45 | // Load initial value 46 | if (this.settingsManager?.fontStyle.isSet()) { 47 | this.selectItem(this.settingsManager.fontStyle.value); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/ts/components/subtitlesettings/subtitlesettingselectbox.ts: -------------------------------------------------------------------------------- 1 | import {SubtitleOverlay} from '../subtitleoverlay'; 2 | import {ListSelectorConfig} from '../listselector'; 3 | import {SelectBox} from '../selectbox'; 4 | import {SubtitleSettingsManager} from './subtitlesettingsmanager'; 5 | import { PlayerAPI } from 'bitmovin-player'; 6 | import { UIInstanceManager } from '../../uimanager'; 7 | 8 | /** 9 | * @category Configs 10 | */ 11 | export interface SubtitleSettingSelectBoxConfig extends ListSelectorConfig { 12 | overlay: SubtitleOverlay; 13 | } 14 | 15 | /** 16 | * Base class for all subtitles settings select box 17 | * 18 | * @category Components 19 | **/ 20 | export class SubtitleSettingSelectBox extends SelectBox { 21 | 22 | protected settingsManager?: SubtitleSettingsManager; 23 | protected overlay: SubtitleOverlay; 24 | private currentCssClass: string; 25 | 26 | constructor(config: SubtitleSettingSelectBoxConfig) { 27 | super(config); 28 | 29 | this.overlay = config.overlay; 30 | } 31 | 32 | /** 33 | * Removes a previously set class and adds the passed in class. 34 | * @param cssClass The new class to replace the previous class with or null to just remove the previous class 35 | */ 36 | protected toggleOverlayClass(cssClass: string|null): void { 37 | // Remove previous class if existing 38 | if (this.currentCssClass) { 39 | this.overlay.getDomElement().removeClass(this.currentCssClass); 40 | this.currentCssClass = null; 41 | } 42 | 43 | // Add new class if specified. If the new class is null, we don't add anything. 44 | if (cssClass) { 45 | this.currentCssClass = this.prefixCss(cssClass); 46 | this.overlay.getDomElement().addClass(this.currentCssClass); 47 | } 48 | } 49 | 50 | configure(player: PlayerAPI, uimanager: UIInstanceManager): void { 51 | this.settingsManager = uimanager.getSubtitleSettingsManager(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/ts/components/subtitlesettings/subtitlesettingslabel.ts: -------------------------------------------------------------------------------- 1 | import {LabelConfig} from '../label'; 2 | import {Container, ContainerConfig} from '../container'; 3 | import {DOM} from '../../dom'; 4 | import {SettingsPanelPageOpenButton} from '../settingspanelpageopenbutton'; 5 | import { LocalizableText, i18n } from '../../localization/i18n'; 6 | 7 | /** 8 | * @category Configs 9 | */ 10 | export interface SubtitleSettingsLabelConfig extends LabelConfig { 11 | opener: SettingsPanelPageOpenButton; 12 | } 13 | 14 | /** 15 | * @category Components 16 | */ 17 | export class SubtitleSettingsLabel extends Container { 18 | 19 | private opener: SettingsPanelPageOpenButton; 20 | 21 | private text: LocalizableText; 22 | 23 | private for: string; 24 | 25 | constructor(config: SubtitleSettingsLabelConfig) { 26 | super(config); 27 | 28 | this.opener = config.opener; 29 | this.text = config.text; 30 | this.for = config.for; 31 | 32 | this.config = this.mergeConfig(config, { 33 | cssClass: 'ui-label', 34 | components: [ 35 | this.opener, 36 | ], 37 | }, this.config); 38 | } 39 | 40 | protected toDomElement(): DOM { 41 | let labelElement = new DOM('label', { 42 | 'id': this.config.id, 43 | 'class': this.getCssClasses(), 44 | 'for': this.for, 45 | }, this).append( 46 | new DOM('span', {}).html(i18n.performLocalization(this.text)), 47 | this.opener.getDomElement(), 48 | ); 49 | 50 | return labelElement; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/ts/components/subtitlesettings/subtitlesettingsresetbutton.ts: -------------------------------------------------------------------------------- 1 | import {UIInstanceManager} from '../../uimanager'; 2 | import {SubtitleSettingsManager} from './subtitlesettingsmanager'; 3 | import {Button, ButtonConfig} from '../button'; 4 | import { PlayerAPI } from 'bitmovin-player'; 5 | import { i18n } from '../../localization/i18n'; 6 | 7 | /** 8 | * A button that resets all subtitle settings to their defaults. 9 | * 10 | * @category Buttons 11 | */ 12 | export class SubtitleSettingsResetButton extends Button { 13 | 14 | private settingsManager: SubtitleSettingsManager; 15 | 16 | constructor(config: ButtonConfig) { 17 | super(config); 18 | 19 | this.config = this.mergeConfig(config, { 20 | cssClass: 'ui-subtitlesettingsresetbutton', 21 | text: i18n.getLocalizer('reset'), 22 | }, this.config); 23 | } 24 | 25 | configure(player: PlayerAPI, uimanager: UIInstanceManager): void { 26 | super.configure(player, uimanager); 27 | this.settingsManager = uimanager.getSubtitleSettingsManager(); 28 | 29 | this.onClick.subscribe(() => { 30 | this.settingsManager.reset(); 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/ts/components/subtitlesettings/windowopacityselectbox.ts: -------------------------------------------------------------------------------- 1 | import { SubtitleSettingSelectBox, SubtitleSettingSelectBoxConfig } from './subtitlesettingselectbox'; 2 | import {UIInstanceManager} from '../../uimanager'; 3 | import { PlayerAPI } from 'bitmovin-player'; 4 | import { i18n } from '../../localization/i18n'; 5 | 6 | /** 7 | * A select box providing a selection of different background opacity. 8 | * 9 | * @category Components 10 | */ 11 | export class WindowOpacitySelectBox extends SubtitleSettingSelectBox { 12 | 13 | constructor(config: SubtitleSettingSelectBoxConfig) { 14 | super(config); 15 | 16 | this.config = this.mergeConfig(config, { 17 | cssClasses: ['ui-subtitlesettingswindowopacityselectbox'], 18 | }, this.config); 19 | } 20 | 21 | configure(player: PlayerAPI, uimanager: UIInstanceManager): void { 22 | super.configure(player, uimanager); 23 | 24 | this.addItem(null, i18n.getLocalizer('default')); 25 | this.addItem('100', i18n.getLocalizer('percent', { value: 100 })); 26 | this.addItem('75', i18n.getLocalizer('percent', { value: 75 })); 27 | this.addItem('50', i18n.getLocalizer('percent', { value: 50 })); 28 | this.addItem('25', i18n.getLocalizer('percent', { value: 25 })); 29 | this.addItem('0', i18n.getLocalizer('percent', { value: 0 })); 30 | 31 | this.onItemSelected.subscribe((sender, key: string) => { 32 | this.settingsManager.windowOpacity.value = key; 33 | 34 | // Color and opacity go together, so we need to... 35 | if (!this.settingsManager.windowOpacity.isSet()) { 36 | // ... clear the color when the opacity is not set 37 | this.settingsManager.windowColor.clear(); 38 | } else if (!this.settingsManager.windowColor.isSet()) { 39 | // ... set a color when the opacity is set 40 | this.settingsManager.windowColor.value = 'black'; 41 | } 42 | }); 43 | 44 | // Update selected item when value is set from somewhere else 45 | this.settingsManager.windowOpacity.onChanged.subscribe((sender, property) => { 46 | this.selectItem(property.value); 47 | }); 48 | 49 | // Load initial value 50 | if (this.settingsManager.windowOpacity.isSet()) { 51 | this.selectItem(this.settingsManager.windowOpacity.value); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/ts/components/volumetogglebutton.ts: -------------------------------------------------------------------------------- 1 | import {ToggleButton, ToggleButtonConfig} from './togglebutton'; 2 | import { UIInstanceManager } from '../uimanager'; 3 | import { PlayerAPI } from 'bitmovin-player'; 4 | import { i18n } from '../localization/i18n'; 5 | 6 | /** 7 | * A button that toggles audio muting. 8 | * 9 | * @category Buttons 10 | */ 11 | export class VolumeToggleButton extends ToggleButton { 12 | 13 | constructor(config: ToggleButtonConfig = {}) { 14 | super(config); 15 | 16 | const defaultConfig: ToggleButtonConfig = { 17 | cssClass: 'ui-volumetogglebutton', 18 | text: i18n.getLocalizer('settings.audio.mute'), 19 | onClass: 'muted', 20 | offClass: 'unmuted', 21 | ariaLabel: i18n.getLocalizer('settings.audio.mute'), 22 | }; 23 | 24 | this.config = this.mergeConfig(config, defaultConfig, this.config); 25 | } 26 | 27 | configure(player: PlayerAPI, uimanager: UIInstanceManager): void { 28 | super.configure(player, uimanager); 29 | 30 | const volumeController = uimanager.getConfig().volumeController; 31 | 32 | volumeController.onChanged.subscribe((_, args) => { 33 | if (args.muted) { 34 | this.on(); 35 | } else { 36 | this.off(); 37 | } 38 | 39 | const volumeLevelTens = Math.ceil(args.volume / 10); 40 | this.getDomElement().data(this.prefixCss('volume-level-tens'), String(volumeLevelTens)); 41 | }); 42 | 43 | this.onClick.subscribe(() => { 44 | volumeController.toggleMuted(); 45 | }); 46 | 47 | // Startup init 48 | volumeController.onChangedEvent(); 49 | } 50 | } -------------------------------------------------------------------------------- /src/ts/components/watermark.ts: -------------------------------------------------------------------------------- 1 | import {ClickOverlay, ClickOverlayConfig} from './clickoverlay'; 2 | import { i18n } from '../localization/i18n'; 3 | 4 | /** 5 | * Configuration interface for a {@link ClickOverlay}. 6 | * 7 | * @category Configs 8 | */ 9 | export interface WatermarkConfig extends ClickOverlayConfig { 10 | // nothing yet 11 | } 12 | 13 | /** 14 | * A watermark overlay with a clickable logo. 15 | * 16 | * @category Components 17 | */ 18 | export class Watermark extends ClickOverlay { 19 | 20 | constructor(config: WatermarkConfig = {}) { 21 | super(config); 22 | 23 | this.config = this.mergeConfig(config, { 24 | cssClass: 'ui-watermark', 25 | url: 'http://bitmovin.com', 26 | role: 'link', 27 | text: 'logo', 28 | ariaLabel: i18n.getLocalizer('watermarkLink'), 29 | }, this.config); 30 | } 31 | } -------------------------------------------------------------------------------- /src/ts/focusvisibilitytracker.ts: -------------------------------------------------------------------------------- 1 | const FocusVisibleCssClassName = '{{PREFIX}}-focus-visible'; 2 | 3 | export class FocusVisibilityTracker { 4 | private readonly eventHandlerMap: { [eventName: string]: EventListenerOrEventListenerObject }; 5 | private lastInteractionWasKeyboard: boolean = true; 6 | 7 | constructor(private bitmovinUiPrefix: string) { 8 | this.eventHandlerMap = { 9 | mousedown: this.onMouseOrPointerOrTouch, 10 | pointerdown: this.onMouseOrPointerOrTouch, 11 | touchstart: this.onMouseOrPointerOrTouch, 12 | keydown: this.onKeyDown, 13 | focus: this.onFocus, 14 | blur: this.onBlur, 15 | }; 16 | this.registerEventListeners(); 17 | } 18 | 19 | private onKeyDown = (e: KeyboardEvent) => { 20 | if (e.metaKey || e.altKey || e.ctrlKey) { 21 | return; 22 | } 23 | 24 | this.lastInteractionWasKeyboard = true; 25 | }; 26 | 27 | private onMouseOrPointerOrTouch = () => (this.lastInteractionWasKeyboard = false); 28 | 29 | private onFocus = ({ target: element }: FocusEvent) => { 30 | if ( 31 | this.lastInteractionWasKeyboard && 32 | isHtmlElement(element) && 33 | isBitmovinUi(element, this.bitmovinUiPrefix) && 34 | !element.classList.contains(FocusVisibleCssClassName) 35 | ) { 36 | element.classList.add(FocusVisibleCssClassName); 37 | } 38 | }; 39 | 40 | private onBlur = ({ target: element }: FocusEvent) => { 41 | if (isHtmlElement(element)) { 42 | element.classList.remove(FocusVisibleCssClassName); 43 | } 44 | }; 45 | 46 | private registerEventListeners(): void { 47 | for (const event in this.eventHandlerMap) { 48 | document.addEventListener(event, this.eventHandlerMap[event], true); 49 | } 50 | } 51 | 52 | private unregisterEventListeners(): void { 53 | for (const event in this.eventHandlerMap) { 54 | document.removeEventListener(event, this.eventHandlerMap[event], true); 55 | } 56 | } 57 | 58 | public release(): void { 59 | this.unregisterEventListeners(); 60 | } 61 | } 62 | 63 | function isBitmovinUi(element: Element, bitmovinUiPrefix: string): boolean { 64 | return element.id.indexOf(bitmovinUiPrefix) === 0; 65 | } 66 | 67 | function isHtmlElement(element: unknown): element is HTMLElement & { classList: DOMTokenList } { 68 | return ( 69 | element instanceof HTMLElement && element.classList instanceof DOMTokenList 70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /src/ts/guid.ts: -------------------------------------------------------------------------------- 1 | export namespace Guid { 2 | 3 | let guid = 1; 4 | 5 | export function next() { 6 | return guid++; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/ts/localization/languages/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings.video.quality": "Videoqualität", 3 | "settings.audio.quality": "Audioqualität", 4 | "settings.audio.track": "Audiospur", 5 | "speed": "Geschwindigkeit", 6 | "play": "Abspielen", 7 | "pause": "Pause", 8 | "playPause": "Abspielen/Pause", 9 | "open": "öffnen", 10 | "close": "Schließen", 11 | "settings.audio.mute": "Stummschaltung", 12 | "settings.audio.volume": "Lautstärke", 13 | "pictureInPicture": "Bild im Bild", 14 | "appleAirplay": "Apple AirPlay", 15 | "googleCast": "Google Cast", 16 | "vr": "VR", 17 | "settings": "Einstellungen", 18 | "fullscreen": "Vollbild", 19 | "off": "aus", 20 | "settings.subtitles": "Untertitel", 21 | "settings.subtitles.font.size": "Größe", 22 | "settings.subtitles.font.style": "Schriftstil", 23 | "settings.subtitles.font.style.bold": "Fett", 24 | "settings.subtitles.font.style.italic": "Kursiv", 25 | "settings.subtitles.font.family": "Schriftart", 26 | "settings.subtitles.font.color": "Farbe", 27 | "settings.subtitles.font.opacity": "Deckkraft", 28 | "settings.subtitles.characterEdge": "Ränder", 29 | "settings.subtitles.characterEdge.color": "Buchstabenrandfarbe", 30 | "settings.subtitles.background.color": "Hintergrundfarbe", 31 | "settings.subtitles.background.opacity": "Hintergrunddeckkraft", 32 | "settings.subtitles.window.color": "Hintergrundfarbe", 33 | "settings.subtitles.window.opacity": "Hintergrunddeckkraft", 34 | "settings.time.hours": "Stunden", 35 | "settings.time.minutes": "Minuten", 36 | "settings.time.seconds": "Sekunden", 37 | "back": "Zurück", 38 | "reset": "Zurücksetzen", 39 | "replay": "Wiederholen", 40 | "ads.remainingTime": "Diese Anzeige endet in {remainingTime} Sekunden", 41 | "default": "standard", 42 | "colors.white": "weiß", 43 | "colors.black": "schwarz", 44 | "colors.red": "rot", 45 | "colors.green": "grün", 46 | "colors.blue": "blau", 47 | "colors.yellow": "gelb", 48 | "subtitle.example": "Beispiel Untertitel", 49 | "subtitle.select": "Untertitel auswählen", 50 | "playingOn": "Spielt auf {castDeviceName}", 51 | "connectingTo": "Verbindung mit {castDeviceName} wird hergestellt...", 52 | "watermarkLink": "Link zum Homepage", 53 | "controlBar": "Videoplayer Kontrollen", 54 | "player": "Video player", 55 | "seekBar": "Video-Timeline", 56 | "seekBar.value": "Wert", 57 | "seekBar.timeshift": "Timeshift", 58 | "seekBar.durationText": "aus", 59 | "quickseek.forward": "{seekSeconds} Sekunden Vor", 60 | "quickseek.rewind": "{seekSeconds} Sekunden Zurück", 61 | "ecoMode": "ecoMode", 62 | "ecoMode.title":"Eco Mode" 63 | } 64 | -------------------------------------------------------------------------------- /src/ts/mobilev3playerapi.ts: -------------------------------------------------------------------------------- 1 | import { PlayerAPI, PlayerEvent, PlayerEventBase, PlayerEventCallback } from 'bitmovin-player'; 2 | import { WrappedPlayer } from './uimanager'; 3 | 4 | export enum MobileV3PlayerEvent { 5 | SourceError = 'sourceerror', 6 | PlayerError = 'playererror', 7 | PlaylistTransition = 'playlisttransition', 8 | } 9 | 10 | export interface MobileV3PlayerErrorEvent extends PlayerEventBase { 11 | code: number; 12 | message: string; 13 | } 14 | 15 | export interface MobileV3SourceErrorEvent extends PlayerEventBase { 16 | code: number; 17 | message: string; 18 | } 19 | 20 | export type MobileV3PlayerEventType = PlayerEvent | MobileV3PlayerEvent; 21 | 22 | export interface MobileV3PlayerAPI extends PlayerAPI { 23 | on(eventType: MobileV3PlayerEventType, callback: PlayerEventCallback): void; 24 | exports: PlayerAPI['exports'] & { PlayerEvent: MobileV3PlayerEventType }; 25 | } 26 | 27 | export function isMobileV3PlayerAPI(player: WrappedPlayer | PlayerAPI | MobileV3PlayerAPI): player is MobileV3PlayerAPI { 28 | for (const key in MobileV3PlayerEvent) { 29 | if (MobileV3PlayerEvent.hasOwnProperty(key) && !player.exports.PlayerEvent.hasOwnProperty(key)) { 30 | return false; 31 | } 32 | } 33 | 34 | return true; 35 | } 36 | -------------------------------------------------------------------------------- /src/ts/spatialnavigation/ListNavigationGroup.ts: -------------------------------------------------------------------------------- 1 | import { NavigationGroup } from './navigationgroup'; 2 | import { Action, Direction } from './types'; 3 | import { Container } from '../components/container'; 4 | import { Component } from '../components/component'; 5 | 6 | export enum ListOrientation { 7 | Horizontal = 'horizontal', 8 | Vertical = 'vertical', 9 | } 10 | 11 | /** 12 | * @category Components 13 | */ 14 | export class ListNavigationGroup extends NavigationGroup { 15 | private readonly listNavigationDirections: Direction[]; 16 | 17 | constructor(orientation: ListOrientation, container: Container, ...components: Component[]) { 18 | super(container, ...components); 19 | 20 | switch (orientation) { 21 | case ListOrientation.Vertical: 22 | this.listNavigationDirections = [Direction.UP, Direction.DOWN]; 23 | break; 24 | 25 | case ListOrientation.Horizontal: 26 | this.listNavigationDirections = [Direction.LEFT, Direction.RIGHT]; 27 | break; 28 | } 29 | } 30 | 31 | public handleAction(action: Action): void { 32 | super.handleAction(action); 33 | 34 | if (action === Action.SELECT) { 35 | // close the container when a list entry is selected 36 | this.handleAction(Action.BACK); 37 | } 38 | } 39 | 40 | public handleNavigation(direction: Direction): void { 41 | super.handleNavigation(direction); 42 | 43 | if (!this.listNavigationDirections.includes(direction)) { 44 | // close the container on navigation inputs that don't align 45 | // with the orientation of the list 46 | this.handleAction(Action.BACK); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/ts/spatialnavigation/gethtmlelementsfromcomponents.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '../components/component'; 2 | import { Container } from '../components/container'; 3 | import { isComponent, isContainer, isListBox } from './typeguards'; 4 | 5 | /** 6 | * Recursively resolves a container and the components contained within them, building a flat list of components. 7 | * 8 | * @param container The container to get the contained components from 9 | */ 10 | function resolveAllComponents(container: Container): Component[] { 11 | const childComponents: Component[] = []; 12 | 13 | container.getComponents().forEach(containerOrComponent => { 14 | if (isContainer(containerOrComponent)) { 15 | childComponents.push(...resolveAllComponents(containerOrComponent)); 16 | } else if (isComponent(containerOrComponent)) { 17 | childComponents.push(containerOrComponent); 18 | } 19 | }); 20 | 21 | return childComponents; 22 | } 23 | 24 | /** 25 | * Returns the HTML elements associated to the provided component. 26 | * 27 | * @param component The component to get the HTML elements from 28 | */ 29 | function toHtmlElement(component: Component): HTMLElement[] { 30 | if (isListBox(component)) { 31 | return [].slice.call(component.getDomElement().get()[0].children); 32 | } else { 33 | return component.getDomElement().get().slice(0, 1); 34 | } 35 | } 36 | 37 | /** 38 | * Takes the provided list of components and flat-maps them to a list of their respective HTML elements. In case a 39 | * provided component is a container, the children of that container will be resolved recursively. Ignores components 40 | * that are hidden. 41 | * 42 | * @param components The components to map to HTML elements 43 | */ 44 | export function getHtmlElementsFromComponents(components: Component[]): HTMLElement[] { 45 | const htmlElements: HTMLElement[] = []; 46 | 47 | components 48 | .filter(component => !component.isHidden()) 49 | .forEach(component => { 50 | const elementsToConsider = component instanceof Container ? resolveAllComponents(component) : [component]; 51 | 52 | elementsToConsider.forEach(component => { 53 | htmlElements.push(...toHtmlElement(component)); 54 | }); 55 | }); 56 | 57 | return htmlElements; 58 | } 59 | -------------------------------------------------------------------------------- /src/ts/spatialnavigation/nodeeventsubscriber.ts: -------------------------------------------------------------------------------- 1 | type Listeners = ([Node, EventListenerOrEventListenerObject, boolean | AddEventListenerOptions])[]; 2 | 3 | /** 4 | * Allows to subscribe to Node events. 5 | */ 6 | export class NodeEventSubscriber { 7 | private readonly attachedListeners: Map; 8 | 9 | constructor() { 10 | this.attachedListeners = new Map(); 11 | } 12 | 13 | private getEventListenersOfType(type: keyof HTMLElementEventMap): Listeners { 14 | if (!this.attachedListeners.has(type)) { 15 | this.attachedListeners.set(type, []); 16 | } 17 | 18 | return this.attachedListeners.get(type); 19 | } 20 | 21 | /** 22 | * Adds the given event listener to the node. 23 | * 24 | * @param node The node to remove the event listener from 25 | * @param type The event to listen to 26 | * @param listener The listener to remove 27 | * @param options The event listener options 28 | */ 29 | public on( 30 | node: Node, 31 | type: keyof HTMLElementEventMap, 32 | listener: EventListenerOrEventListenerObject, 33 | options?: boolean | AddEventListenerOptions, 34 | ): void { 35 | node.addEventListener(type, listener, options); 36 | this.getEventListenersOfType(type).push([node, listener, options]); 37 | } 38 | 39 | /** 40 | * Removes the given event listener from the node. 41 | * 42 | * @param node The node to attach the event listener to 43 | * @param type The event to listen to 44 | * @param listener The listener to add 45 | * @param options The event listener options 46 | */ 47 | public off( 48 | node: Node, 49 | type: keyof HTMLElementEventMap, 50 | listener: EventListenerOrEventListenerObject, 51 | options?: boolean | AddEventListenerOptions, 52 | ): void { 53 | const listenersOfType = this.getEventListenersOfType(type); 54 | const listenerIndex = listenersOfType.findIndex(([otherNode, otherListener, otherOptions]) => { 55 | return otherNode === node && otherListener === listener && otherOptions === options; 56 | }); 57 | 58 | node.removeEventListener(type, listener, options); 59 | 60 | if (listenerIndex > -1) { 61 | listenersOfType.splice(listenerIndex, 1); 62 | } 63 | } 64 | 65 | /** 66 | * Removes all attached event listeners. 67 | */ 68 | public release(): void { 69 | this.attachedListeners.forEach((listenersOfType, type) => { 70 | listenersOfType.forEach(([element, listener, options]) => { 71 | this.off(element, type, listener, options); 72 | }); 73 | }); 74 | this.attachedListeners.clear(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/ts/spatialnavigation/rootnavigationgroup.ts: -------------------------------------------------------------------------------- 1 | import { NavigationGroup } from './navigationgroup'; 2 | import { Component } from '../components/component'; 3 | import { UIContainer } from '../components/uicontainer'; 4 | import { Action, Direction } from './types'; 5 | 6 | /** 7 | * Extends NavigationGroup and provides additional logic for hiding and showing the UI on the root container. 8 | * 9 | * @category Components 10 | */ 11 | export class RootNavigationGroup extends NavigationGroup { 12 | constructor(public readonly container: UIContainer, ...elements: Component[]) { 13 | super(container, ...elements); 14 | } 15 | 16 | public handleAction(action: Action) { 17 | this.container.showUi(); 18 | 19 | super.handleAction(action); 20 | } 21 | 22 | public handleNavigation(direction: Direction) { 23 | this.container.showUi(); 24 | 25 | super.handleNavigation(direction); 26 | } 27 | 28 | protected defaultActionHandler(action: Action): void { 29 | if (action === Action.BACK) { 30 | this.container.hideUi(); 31 | } else { 32 | super.defaultActionHandler(action); 33 | } 34 | } 35 | 36 | public release(): void { 37 | super.release(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/ts/spatialnavigation/typeguards.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '../components/component'; 2 | import { SettingsPanel } from '../components/settingspanel'; 3 | import { Container } from '../components/container'; 4 | import { ListBox } from '../components/listbox'; 5 | import { Action, Direction } from './types'; 6 | 7 | export function isSettingsPanel(component: Component): component is SettingsPanel { 8 | return component instanceof SettingsPanel; 9 | } 10 | 11 | export function isComponent(obj: unknown): obj is Component { 12 | return obj !== null && obj !== undefined && obj instanceof Component; 13 | } 14 | 15 | export function isContainer(obj: unknown): obj is Container { 16 | return obj !== null && obj !== undefined && obj instanceof Container; 17 | } 18 | 19 | export function isListBox(obj: unknown): obj is ListBox { 20 | return obj instanceof ListBox; 21 | } 22 | 23 | export function isDirection(direction: unknown): direction is Direction { 24 | return typeof direction === 'string' && Object.values(Direction).includes(direction); 25 | } 26 | 27 | export function isAction(action: unknown): action is Action { 28 | return typeof action === 'string' && Object.values(Action).includes(action); 29 | } 30 | -------------------------------------------------------------------------------- /src/ts/spatialnavigation/types.ts: -------------------------------------------------------------------------------- 1 | export type Callback = (data: T, target: HTMLElement, preventDefault: () => void) => void; 2 | export type NavigationCallback = Callback; 3 | export type ActionCallback = Callback; 4 | export type KeyMap = { 5 | [keyCode: number]: Action | Direction; 6 | }; 7 | 8 | export enum Direction { 9 | UP = 'up', 10 | DOWN = 'down', 11 | LEFT = 'left', 12 | RIGHT = 'right', 13 | } 14 | 15 | export enum Action { 16 | SELECT = 'select', 17 | BACK = 'back', 18 | } 19 | -------------------------------------------------------------------------------- /src/ts/uiutils.ts: -------------------------------------------------------------------------------- 1 | import {Component, ComponentConfig} from './components/component'; 2 | import {Container} from './components/container'; 3 | 4 | /** 5 | * @category Utils 6 | */ 7 | export namespace UIUtils { 8 | export interface TreeTraversalCallback { 9 | (component: Component, parent?: Component): void; 10 | } 11 | 12 | export function traverseTree(component: Component, visit: TreeTraversalCallback): void { 13 | let recursiveTreeWalker = (component: Component, parent?: Component) => { 14 | visit(component, parent); 15 | 16 | // If the current component is a container, visit it's children 17 | if (component instanceof Container) { 18 | for (let childComponent of component.getComponents()) { 19 | recursiveTreeWalker(childComponent, component); 20 | } 21 | } 22 | }; 23 | 24 | // Walk and configure the component tree 25 | recursiveTreeWalker(component); 26 | } 27 | 28 | // From: https://github.com/nfriend/ts-keycode-enum/blob/master/Key.enum.ts 29 | export enum KeyCode { 30 | LeftArrow = 37, 31 | UpArrow = 38, 32 | RightArrow = 39, 33 | DownArrow = 40, 34 | Space = 32, 35 | End = 35, 36 | Home = 36, 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "src/ts/main.ts" 4 | ], 5 | "compilerOptions": { 6 | "noImplicitAny": true, 7 | "target": "es5", 8 | "declaration": true, 9 | "outDir": "dist/js/framework", 10 | "lib": ["es6", "dom", "scripthost"], 11 | "skipLibCheck": true, 12 | // To be able to import Json Files. 13 | "resolveJsonModule": true, 14 | "esModuleInterop": true 15 | } 16 | } -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "out": "./docs/", 3 | "entryPoints": [ 4 | "src/ts/main.ts" 5 | ], 6 | "tsconfig": "./tsconfig.json", 7 | "defaultCategory": "UI Framework", 8 | "navigation": { 9 | "includeCategories": true, 10 | "includeGroups": true 11 | }, 12 | "excludeProtected": true, 13 | "categoryOrder": [ 14 | "UI Framework", 15 | "Configs", 16 | "Components", 17 | "Buttons", 18 | "Labels", 19 | "Containers", 20 | "Localization", 21 | "Utils" 22 | ] 23 | } 24 | --------------------------------------------------------------------------------