├── .csscomb.json ├── .editorconfig ├── .gitignore ├── .npmignore ├── .npmrc ├── .nvmrc ├── .prettierrc ├── .storybook ├── addons.js ├── config.js └── webpack.config.js ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── LINKS.md ├── README.md ├── dev-env ├── karma-with-adapters.conf.js ├── karma.conf.base.js └── karma.conf.js ├── docs ├── 2.0.0-migration.md ├── adapters.md ├── api.md ├── docs.yml ├── events.md ├── events │ ├── errors.md │ ├── live-states.md │ ├── playback-states.md │ ├── ui-events.md │ └── video-events.md ├── favicon.ico ├── index.md ├── logo.png ├── logo_small.png ├── md-components │ ├── PlayableDemo.js │ ├── PlayableDemo.module.scss │ └── index.js ├── migration.md ├── modules.md ├── player-config.md ├── player-public-methods.md ├── player-texts.md ├── site.yml ├── themes.md └── video-source.md ├── package.json ├── pom.xml ├── scripts ├── documentation │ ├── lib │ │ └── player │ │ │ ├── createPlayerApiClassMethod.js │ │ │ ├── createPlayerApiVisitor.js │ │ │ └── playerApi.js │ └── playerApiVisitor.js ├── npm-release.js └── npm-version.js ├── src ├── adapters │ ├── dash.ts │ └── hls.ts ├── constants │ ├── engine-state.ts │ ├── errors.ts │ ├── events │ │ ├── ui.ts │ │ └── video.ts │ ├── index.ts │ ├── live-state.ts │ ├── media-stream.ts │ └── text-labels.ts ├── core │ ├── config.spec.ts │ ├── config.ts │ ├── default-modules.ts │ ├── dependency-container │ │ ├── constants │ │ │ └── Lifetime.ts │ │ ├── createContainer.spec.ts │ │ ├── createContainer.ts │ │ ├── errors │ │ │ ├── ExtendableError.ts │ │ │ ├── NotAFunctionError.ts │ │ │ └── ResolutionError.ts │ │ ├── index.ts │ │ ├── registrations.spec.ts │ │ ├── registrations.ts │ │ ├── types.ts │ │ └── utils │ │ │ └── nameValueToObject.ts │ ├── playable-module.ts │ ├── player-api-decorator.spec.ts │ ├── player-api-decorator.ts │ ├── player-facade.spec.ts │ ├── player-facade.ts │ ├── player-factory.spec.ts │ └── player-factory.ts ├── develop.ts ├── e2e │ ├── node.js │ ├── playback-test-with-adapters.ts │ └── playback-test.ts ├── index.html ├── index.stories.tsx ├── index.ts ├── modules │ ├── anomaly-bloodhound │ │ ├── anomaly-bloodhound.spec.ts │ │ ├── anomaly-bloodhound.ts │ │ └── types.ts │ ├── chromecast-manager │ │ ├── chromecast-manager.spec.ts │ │ ├── chromecast-manager.ts │ │ └── types.ts │ ├── event-emitter │ │ ├── event-emitter.spec.ts │ │ ├── event-emitter.ts │ │ └── types.ts │ ├── full-screen-manager │ │ ├── desktop.spec.ts │ │ ├── desktop.ts │ │ ├── full-screen-manager.spec.ts │ │ ├── full-screen-manager.ts │ │ ├── ios.spec.ts │ │ ├── ios.ts │ │ └── types.ts │ ├── keyboard-control │ │ ├── keyboard-control.spec.ts │ │ ├── keyboard-control.ts │ │ └── types.ts │ ├── picture-in-picture │ │ ├── chrome.spec.ts │ │ ├── chrome.ts │ │ ├── picture-in-picture.spec.ts │ │ ├── picture-in-picture.ts │ │ ├── safari.spec.ts │ │ ├── safari.ts │ │ └── types.ts │ ├── playback-engine │ │ ├── live-state-engine.spec.ts │ │ ├── live-state-engine.ts │ │ ├── output │ │ │ ├── chromecast │ │ │ │ ├── chromecast-output.ts │ │ │ │ ├── state-engine.ts │ │ │ │ └── types.ts │ │ │ └── native │ │ │ │ ├── adapters-strategy.spec.ts │ │ │ │ ├── adapters-strategy.ts │ │ │ │ ├── adapters │ │ │ │ ├── default-set.ts │ │ │ │ ├── native.ts │ │ │ │ └── types.ts │ │ │ │ ├── html5video-output.ts │ │ │ │ ├── native-events-broadcaster.spec.ts │ │ │ │ ├── native-events-broadcaster.ts │ │ │ │ ├── state-engine.spec.ts │ │ │ │ └── state-engine.ts │ │ ├── playback-engine.spec.ts │ │ ├── playback-engine.ts │ │ ├── types.ts │ │ └── utils │ │ │ ├── adapters-resolver.spec.ts │ │ │ ├── adapters-resolver.ts │ │ │ ├── detect-stream-type.spec.ts │ │ │ └── detect-stream-type.ts │ ├── root-container │ │ ├── normalize │ │ │ ├── _button.scss │ │ │ └── _universal.scss │ │ ├── root-container.scss │ │ ├── root-container.spec.ts │ │ ├── root-container.ts │ │ ├── root-container.view.ts │ │ ├── templates │ │ │ ├── container.dot │ │ │ └── index.ts │ │ ├── types.ts │ │ └── utils │ │ │ ├── focus-source.ts │ │ │ ├── focus-within.ts │ │ │ ├── interaction-type.spec.ts │ │ │ └── interaction-type.ts │ ├── text-map │ │ ├── default-texts.ts │ │ ├── text-map.spec.ts │ │ ├── text-map.ts │ │ └── types.ts │ └── ui │ │ ├── bottom-block │ │ ├── bottom-block.scss │ │ ├── bottom-block.spec.ts │ │ ├── bottom-block.ts │ │ ├── bottom-block.view.ts │ │ ├── templates │ │ │ ├── bottom-block.dot │ │ │ └── index.ts │ │ └── types.ts │ │ ├── conditions.scss │ │ ├── controls │ │ ├── chromecast │ │ │ ├── chromecast.scss │ │ │ ├── chromecast.theme.ts │ │ │ ├── chromecast.ts │ │ │ ├── chromecast.view.ts │ │ │ ├── templates │ │ │ │ ├── chromecast.dot │ │ │ │ └── index.ts │ │ │ └── types.ts │ │ ├── download │ │ │ ├── download.scss │ │ │ ├── download.theme.ts │ │ │ ├── download.ts │ │ │ ├── download.view.ts │ │ │ ├── templates │ │ │ │ ├── control.dot │ │ │ │ └── index.ts │ │ │ └── types.ts │ │ ├── full-screen │ │ │ ├── full-screen.scss │ │ │ ├── full-screen.spec.ts │ │ │ ├── full-screen.theme.ts │ │ │ ├── full-screen.ts │ │ │ ├── full-screen.view.ts │ │ │ ├── templates │ │ │ │ ├── control.dot │ │ │ │ └── index.ts │ │ │ └── types.ts │ │ ├── logo │ │ │ ├── logo.scss │ │ │ ├── logo.theme.ts │ │ │ ├── logo.ts │ │ │ ├── logo.view.ts │ │ │ ├── templates │ │ │ │ ├── index.ts │ │ │ │ ├── logo-button.dot │ │ │ │ ├── logo-image.dot │ │ │ │ ├── logo-input.dot │ │ │ │ └── logo.dot │ │ │ └── types.ts │ │ ├── picture-in-picture │ │ │ ├── picture-in-picture.scss │ │ │ ├── picture-in-picture.spec.ts │ │ │ ├── picture-in-picture.theme.ts │ │ │ ├── picture-in-picture.ts │ │ │ ├── picture-in-picture.view.ts │ │ │ ├── templates │ │ │ │ ├── control.dot │ │ │ │ └── index.ts │ │ │ └── types.ts │ │ ├── play │ │ │ ├── play.scss │ │ │ ├── play.spec.ts │ │ │ ├── play.theme.ts │ │ │ ├── play.ts │ │ │ ├── play.view.ts │ │ │ ├── templates │ │ │ │ ├── control.dot │ │ │ │ └── index.ts │ │ │ └── types.ts │ │ ├── progress │ │ │ ├── progress.scss │ │ │ ├── progress.spec.ts │ │ │ ├── progress.theme.ts │ │ │ ├── progress.ts │ │ │ ├── progress.view.ts │ │ │ ├── templates │ │ │ │ ├── index.ts │ │ │ │ ├── progress.dot │ │ │ │ └── progressTimeIndicator.dot │ │ │ ├── types.ts │ │ │ └── utils │ │ │ │ └── getProgressTimeTooltipPosition.ts │ │ ├── time │ │ │ ├── templates │ │ │ │ ├── index.ts │ │ │ │ └── time.dot │ │ │ ├── time.scss │ │ │ ├── time.spec.ts │ │ │ ├── time.theme.ts │ │ │ ├── time.ts │ │ │ ├── time.view.ts │ │ │ └── types.ts │ │ └── volume │ │ │ ├── templates │ │ │ ├── control.dot │ │ │ └── index.ts │ │ │ ├── types.ts │ │ │ ├── volume.scss │ │ │ ├── volume.spec.ts │ │ │ ├── volume.theme.ts │ │ │ ├── volume.ts │ │ │ └── volume.view.ts │ │ ├── core │ │ ├── element-queries │ │ │ ├── element-queries.ts │ │ │ ├── getQueriesForElement.ts │ │ │ ├── index.ts │ │ │ ├── isElementMatchesSelector.ts │ │ │ └── utils.ts │ │ ├── extendStyles.ts │ │ ├── getElementByHook.ts │ │ ├── htmlToElement.spec.ts │ │ ├── htmlToElement.ts │ │ ├── stylable.spec.ts │ │ ├── stylable.ts │ │ ├── theme │ │ │ ├── index.ts │ │ │ ├── style-sheet.ts │ │ │ ├── theme-service.ts │ │ │ ├── types.ts │ │ │ └── utils │ │ │ │ ├── camelToKebab.ts │ │ │ │ ├── generateClassNames.ts │ │ │ │ ├── getUniqueId.ts │ │ │ │ ├── hexToRgb.ts │ │ │ │ └── transperentizeColor.ts │ │ ├── toggleElementClass.ts │ │ ├── tooltip │ │ │ ├── index.ts │ │ │ ├── templates │ │ │ │ ├── index.ts │ │ │ │ ├── tooltip.dot │ │ │ │ └── tooltipContainer.dot │ │ │ ├── tooltip-container.scss │ │ │ ├── tooltip-container.ts │ │ │ ├── tooltip-reference.ts │ │ │ ├── tooltip-service.ts │ │ │ ├── tooltip.scss │ │ │ ├── tooltip.ts │ │ │ ├── types.ts │ │ │ └── utils │ │ │ │ └── getTooltipPositionByReferenceElement.ts │ │ ├── types.ts │ │ ├── utils │ │ │ ├── formatTime.spec.ts │ │ │ └── formatTime.ts │ │ └── view.ts │ │ ├── debug-panel │ │ ├── debug-panel.scss │ │ ├── debug-panel.ts │ │ ├── debug-panel.view.ts │ │ ├── syntaxHighlight.ts │ │ ├── templates │ │ │ ├── debug-panel.dot │ │ │ └── index.ts │ │ └── types.ts │ │ ├── interaction-indicator │ │ ├── interaction-indicator.scss │ │ ├── interaction-indicator.ts │ │ ├── interaction-indicator.view.ts │ │ ├── templates │ │ │ ├── container.dot │ │ │ ├── decreaseVolumeIcon.dot │ │ │ ├── forwardIcon.dot │ │ │ ├── increaseVolumeIcon.dot │ │ │ ├── index.ts │ │ │ ├── muteIcon.dot │ │ │ ├── pauseIcon.dot │ │ │ ├── playIcon.dot │ │ │ └── rewindIcon.dot │ │ └── types.ts │ │ ├── live-indicator │ │ ├── live-indicator.scss │ │ ├── live-indicator.spec.ts │ │ ├── live-indicator.ts │ │ ├── live-indicator.view.ts │ │ ├── templates │ │ │ ├── index.ts │ │ │ └── live-indicator.dot │ │ └── types.ts │ │ ├── loader │ │ ├── loader.scss │ │ ├── loader.spec.ts │ │ ├── loader.ts │ │ ├── loader.view.ts │ │ ├── templates │ │ │ ├── index.ts │ │ │ └── loader.dot │ │ └── types.ts │ │ ├── loading-cover │ │ ├── loading-cover.scss │ │ ├── loading-cover.spec.ts │ │ ├── loading-cover.ts │ │ ├── loading-cover.view.ts │ │ ├── templates │ │ │ ├── index.ts │ │ │ └── loading-cover.dot │ │ └── types.ts │ │ ├── main-ui-block │ │ ├── main-ui-block.scss │ │ ├── main-ui-block.spec.ts │ │ ├── main-ui-block.ts │ │ ├── main-ui-block.view.ts │ │ ├── templates │ │ │ ├── index.ts │ │ │ └── mainUIBlock.dot │ │ └── types.ts │ │ ├── overlay │ │ ├── overlay.scss │ │ ├── overlay.spec.ts │ │ ├── overlay.theme.ts │ │ ├── overlay.ts │ │ ├── overlay.view.ts │ │ ├── templates │ │ │ ├── index.ts │ │ │ └── overlay.dot │ │ └── types.ts │ │ ├── preview-full-size │ │ ├── preview-full-size.scss │ │ ├── preview-full-size.ts │ │ ├── preview-full-size.view.ts │ │ ├── templates │ │ │ ├── index.ts │ │ │ └── preview.dot │ │ └── types.ts │ │ ├── preview-service │ │ ├── adapter.ts │ │ ├── preview-service.ts │ │ └── types.ts │ │ ├── preview-thumbnail │ │ ├── preview-thumbnail.scss │ │ ├── preview-thumbnail.ts │ │ ├── preview-thumbnail.view.ts │ │ ├── templates │ │ │ ├── index.ts │ │ │ └── thumbnail.dot │ │ └── types.ts │ │ ├── screen │ │ ├── screen.scss │ │ ├── screen.spec.ts │ │ ├── screen.ts │ │ ├── screen.view.ts │ │ ├── templates │ │ │ ├── index.ts │ │ │ └── screen.dot │ │ └── types.ts │ │ ├── shared.scss │ │ ├── subtitles │ │ ├── subtitles.scss │ │ ├── subtitles.ts │ │ ├── subtitles.view.ts │ │ ├── templates │ │ │ ├── index.ts │ │ │ ├── subtitle.dot │ │ │ └── subtitles.dot │ │ └── types.ts │ │ ├── title │ │ ├── templates │ │ │ ├── index.ts │ │ │ └── title.dot │ │ ├── title.scss │ │ ├── title.spec.ts │ │ ├── title.theme.ts │ │ ├── title.ts │ │ ├── title.view.ts │ │ └── types.ts │ │ └── top-block │ │ ├── templates │ │ ├── index.ts │ │ └── top-block.dot │ │ ├── top-block.scss │ │ ├── top-block.ts │ │ ├── top-block.view.ts │ │ └── types.ts ├── stories │ ├── constants.ts │ ├── createPlayerStory.ts │ └── types.ts ├── testkit │ ├── chomecast-api-mock.ts │ └── index.ts ├── typings │ ├── externals.d.ts │ └── internals.d.ts ├── utils │ ├── device-detection.ts │ ├── environment-detection.ts │ ├── get-mime-type.ts │ ├── keyboard-interceptor.spec.ts │ ├── keyboard-interceptor.ts │ ├── logger.ts │ ├── promise.ts │ ├── script-injector.ts │ ├── video-data.spec.ts │ └── video-data.ts ├── with-dash.ts └── with-hls.ts ├── tsconfig.json ├── tslint-rules ├── playerApiRule.ts └── utils │ ├── ast.ts │ └── isPlayerApiDecorator.ts ├── tslint.json ├── wallaby.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | # A special property that should be specified at the top of the file outside of 4 | # any sections. Set to true to stop .editor config file search on current file 5 | root = true 6 | 7 | [*] 8 | # Indentation style 9 | # Possible values - tab, space 10 | indent_style = space 11 | 12 | # Indentation size in single-spaced characters 13 | # Possible values - an integer, tab 14 | indent_size = 2 15 | 16 | # Line ending file format 17 | # Possible values - lf, crlf, cr 18 | end_of_line = lf 19 | 20 | # File character encoding 21 | # Possible values - latin1, utf-8, utf-16be, utf-16le 22 | charset = utf-8 23 | 24 | # Denotes whether to trim whitespace at the end of lines 25 | # Possible values - true, false 26 | trim_trailing_whitespace = true 27 | 28 | # Denotes whether file should end with a newline 29 | # Possible values - true, false 30 | insert_final_newline = true 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | sitedist 3 | node_modules 4 | target 5 | .idea 6 | .DS_Store 7 | .rpt2_cache 8 | yarn-error.log 9 | package-lock.json 10 | storybook-static 11 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | target 2 | node_modules 3 | dist/src 4 | .npmrc 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | registry=https://registry.npmjs.org/ 3 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 14.15.3 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-viewport/register'; 2 | import '@storybook/addon-knobs/register'; 3 | import '@storybook/addon-a11y/register'; 4 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { addDecorator, addParameters, configure } from '@storybook/html'; 2 | import { withA11y } from '@storybook/addon-a11y'; 3 | import { withKnobs } from '@storybook/addon-knobs'; 4 | import { create } from '@storybook/theming'; 5 | 6 | addParameters({ 7 | options: { 8 | theme: create({ 9 | base: 'light', 10 | brandTitle: 'Playable', 11 | brandUrl: 'https://github.com/wix/playable', 12 | brandImage: null, 13 | }), 14 | panelPosition: 'right', 15 | showNav: false, 16 | }, 17 | }); 18 | 19 | addDecorator(withA11y); 20 | addDecorator(withKnobs); 21 | 22 | configure(require.context('../src', true, /\.stories\.tsx$/), module); 23 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | const { DOTJS_OPTIONS } = require('haste-preset-playable/src/config/constants'); 2 | 3 | module.exports = ({ config }) => { 4 | config.module.rules = [ 5 | ...config.module.rules, 6 | require('haste-preset-playable/src/loaders/typescript')(), 7 | require('haste-preset-playable/src/loaders/dot')(DOTJS_OPTIONS), 8 | require('haste-preset-playable/src/loaders/assets')(), 9 | require('haste-preset-playable/src/loaders/svg')(), 10 | ...require('haste-preset-playable/src/loaders/sass')({}), 11 | ]; 12 | 13 | config.resolve.extensions.push('.ts', '.tsx'); 14 | return config; 15 | }; 16 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Playable 2 | 3 | ## Commit Message Guidelines 4 | 5 | We use [conventionalcommits](https://conventionalcommits.org) to format our commit messages. This leads to **more 6 | readable messages** that are easy to follow when looking through the **project history**. But also, 7 | we use the git commit messages to generate the [Playable release notes](https://github.com/wix/playable/releases). 8 | 9 | Each commit message consists of a **header**, a **body** and a **footer**. The header has a special 10 | format that includes a **type**, a **scope** and a **subject**: 11 | 12 | ``` 13 | [optional scope]: 14 | 15 | [optional body] 16 | 17 | [optional footer] 18 | ``` 19 | 20 | More info about message format and list of types could be found [here](https://conventionalcommits.org) 21 | 22 | ### Scope 23 | The scope should be the name of affected playable part (readable for the person reading the changelog generated from commit messages). 24 | 25 | The following is the list of supported scopes: 26 | 27 | * **core** 28 | * **adapters** 29 | * **constants** 30 | * **modules/NAME_OF_MODULE_HERE** 31 | * **modules/ui/NAME_OF_MODULE_HERE** 32 | 33 | 34 | ## Run playground 35 | - `npm i` 36 | - `npm run start` 37 | - open 38 | 39 | ## Check LIVE video stream 40 | Open 41 | 42 | * **Flow:** 43 | - click START 44 | - use button 'LIVE' on localhost to check player video 45 | - click END when want to stop stream 46 | 47 | ## Release New Version 48 | 49 | To release new version: 50 | - checkout latest master version: 51 | `git checkout master` & `git pull` 52 | - run [version script](https://docs.npmjs.com/cli/version): 53 | `npm version [major | minor | patch]` 54 | - push master branch and tags: 55 | `git push origin master` & `git push origin [NEW_VERSION_TAG]` 56 | - draft a new [release on github](https://github.com/wix/playable/releases) with changelog from `npm version` command 57 | - after CI released new package, publish [release on github](https://github.com/wix/playable/releases) 58 | - publish new documentation version `npm run documentation:site:deploy` 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Wix.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LINKS.md: -------------------------------------------------------------------------------- 1 | # Relevant links 2 | 3 | - [[github] Repo](https://github.com/wix/playable) 4 | - [[react] React wrapper](https://github.com/wix-incubator/react-playable) 5 | - [[travis] Travis CI](https://travis-ci.org/wix/playable) 6 | - [[doc] Documentation](https://wix.github.io/playable/) 7 | - [[jira] Jira](https://jira.wixpress.com/browse/VP) 8 | - [[tc] TC CI](http://tc.dev.wixpress.com/viewType.html?buildTypeId=Playable_Playable) 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Playable logo 4 | 5 |

6 | 7 |

8 | 9 | Playable 10 | 11 |

12 | 13 |

14 | 15 | Build Status 16 | 17 | 18 | npm 19 | 20 |

21 | 22 | **IMPORTANT!** Migration guide from 1.0.0 to 2.0.0 you can find [here](/docs/2.0.0-migration.md). 23 | 24 | You can play with demo here: [https://jsfiddle.net/bodia/to0r65f4/](https://jsfiddle.net/bodia/to0r65f4/) 25 | 26 | ## Get it 27 | 28 | ``` 29 | $ npm install playable --save 30 | ``` 31 | 32 | ## Use it 33 | 34 | In modern way 35 | 36 | ```javascript 37 | import Playable from 'playable'; 38 | ``` 39 | 40 | Or in old school way, add a ` 44 | ``` 45 | 46 | And write awesome code: 47 | 48 | ```javascript 49 | document.addEventListener('DOMContentLoaded', function() { 50 | const config = { 51 | width: 700, 52 | height: 394, 53 | src: 'http://my-url/video.mp4', 54 | preload: 'metadata', 55 | }; 56 | const player = Playable.create(config); 57 | 58 | player.attachToElement(document.getElementById('content')); 59 | }); 60 | ``` 61 | 62 | You can find documentation here: [https://wix-incubator.github.io/playable/](https://wix-incubator.github.io/playable/) 63 | 64 | ## Big thanks! 65 | 66 | Cross-browser Testing Platform and Open Source <3 Provided by [Sauce Labs][sauselabs-homepage] 67 | 68 | [sauselabs-homepage]: https://saucelabs.com 69 | [documentation]: https://wix-incubator.github.io/playable/ 70 | -------------------------------------------------------------------------------- /dev-env/karma-with-adapters.conf.js: -------------------------------------------------------------------------------- 1 | const configBase = require('./karma.conf.base'); 2 | 3 | /* ignore coverage */ 4 | module.exports = function(config) { 5 | configBase(config, { 6 | testName: 'Playable with adapters', 7 | }); 8 | 9 | config.set({ 10 | // list of files / patterns to load in the browser 11 | files: ['../dist/statics/playable-test-with-adapters.bundle.js'], 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /dev-env/karma.conf.js: -------------------------------------------------------------------------------- 1 | const configBase = require('./karma.conf.base'); 2 | 3 | /* ignore coverage */ 4 | module.exports = function(config) { 5 | configBase(config, { 6 | testName: 'Playable', 7 | }); 8 | 9 | config.set({ 10 | // list of files / patterns to load in the browser 11 | files: ['../dist/statics/playable-test.bundle.js'], 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "API reference" 3 | include: 4 | - ./player-public-methods.md 5 | --- 6 | -------------------------------------------------------------------------------- /docs/docs.yml: -------------------------------------------------------------------------------- 1 | - path: player-public-methods.md 2 | title: Public methods 3 | entry: src/core/default-modules.ts 4 | glob: src/**/*.ts 5 | visitor: scripts/documentation/playerApiVisitor.js 6 | -------------------------------------------------------------------------------- /docs/events.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Player events" 3 | include: 4 | - ./events/video-events.md 5 | - ./events/ui-events.md 6 | - ./events/errors.md 7 | - ./events/playback-states.md 8 | - ./events/live-states.md 9 | --- 10 | 11 | # Player events 12 | 13 | ```javascript 14 | // Use it from Playable object 15 | import Playable from 'playable'; 16 | 17 | console.log(Playable.VIDEO_EVENTS); 18 | 19 | // Use destruction 20 | import { VIDEO_EVENTS } from 'playable'; 21 | 22 | console.log(VIDEO_EVENTS); 23 | ``` 24 | 25 | > Add new event listeners 26 | 27 | ```javascript 28 | player.on(Playable.UI_EVENTS.PLAY_CLICK, () => { 29 | // Will be executed after you will click on play button 30 | }); 31 | 32 | // To supply a context value for `this` when the callback is invoked, 33 | // pass the optional context argument 34 | player.on(Playable.VIDEO_EVENTS.UPLOAD_STALLED, this.handleStalledUpload, this); 35 | ``` 36 | 37 | > And remove them 38 | 39 | ```javascript 40 | const callback = function() { 41 | // Code to handle some kind of event 42 | }; 43 | 44 | // ... Now callback will be called when some one will pause the video ... 45 | player.on(Playable.UI_EVENTS.PAUSE_CLICK, callback); 46 | 47 | // ... callback will no longer be called. 48 | player.off(Playable.UI_EVENTS.PAUSE_CLICK, callback); 49 | 50 | // ... remove all handlers for event UI_EVENTS.PAUSE_CLICK. 51 | player.off(Playable.UI_EVENTS.PAUSE_CLICK); 52 | ``` 53 | 54 | You can create listeners for events triggered by the video player, using [on](/api#on) method. To remove a listener, use [off](/api#off). 55 | 56 | Below you can see the events that can be passed as eventName. 57 | -------------------------------------------------------------------------------- /docs/events/errors.md: -------------------------------------------------------------------------------- 1 | ### Errors 2 | 3 | `ERRORS.MANIFEST_LOAD` - Cannot load manifest.
4 | `ERRORS.MANIFEST_PARSE` - Cannot parse manifest.
5 | `ERRORS.MANIFEST_INCOMPATIBLE` - Current system is not fit requirements of manifest (maybe some codec is missed or does not work on the system).
6 | `ERRORS.LEVEL_LOAD` - Cannot load level of video.
7 | `ERRORS.CONTENT_LOAD` - Cannot load content of video.
8 | `ERRORS.MEDIA` - Problem with playing video.
9 | `ERRORS.UNKNOWN` - Unknown error.
10 | -------------------------------------------------------------------------------- /docs/events/live-states.md: -------------------------------------------------------------------------------- 1 | ### Live States 2 | 3 | `LIVE_STATES.NONE` - Video is not live stream or metadata is not loaded yet.
4 | `LIVE_STATES.INITIAL` - Player loaded metadata and video is live stream.
5 | `LIVE_STATES.NOT_SYNC` - Video is live stream but not sync with live.
6 | `LIVE_STATES.SYNC` - Video is live stream and sync with live.
7 | `LIVE_STATES.ENDED` - Video is live stream but stream is ended.
8 | -------------------------------------------------------------------------------- /docs/events/playback-states.md: -------------------------------------------------------------------------------- 1 | ### Playback States 2 | 3 | `ENGINE_STATES.SRC_SET` - Source of video setted in player.
4 | `ENGINE_STATES.LOAD_STARTED` - Player started loading of video data.
5 | `ENGINE_STATES.METADATA_LOADED` - Player loaded video metadata.
6 | `ENGINE_STATES.READY_TO_PLAY` - Player ready to play something.
7 | `ENGINE_STATES.SEEK_IN_PROGRESS` - Seek in progress.
8 | `ENGINE_STATES.PLAY_REQUESTED` - Player was requested for play.
9 | `ENGINE_STATES.WAITING` - Player is waiting for content to download.
10 | `ENGINE_STATES.PLAYING` - Player is playing video.
11 | `ENGINE_STATES.PAUSED` - Player paused video.
12 | `ENGINE_STATES.ENDED` - Video ended.
13 | -------------------------------------------------------------------------------- /docs/events/video-events.md: -------------------------------------------------------------------------------- 1 | ### Video Events 2 | 3 | `VIDEO_EVENTS.ERROR` - Error occured. You can check all errors [below](#errors).
4 | 5 | `VIDEO_EVENTS.STATE_CHANGED` - Playback state changed. You can check all states [below](#playback-states).
6 | `VIDEO_EVENTS.DYNAMIC_CONTENT_ENDED` - Live stream ended. Use `VIDEO_EVENTS.LIVE_STATE_CHANGED` for more live states.
7 | `VIDEO_EVENTS.LIVE_STATE_CHANGED` - Live video state changed. You can check all states [below](#live-states).
8 | 9 | `VIDEO_EVENTS.CHUNK_LOADED` - Chunk of video loaded. 10 | 11 | `VIDEO_EVENTS.CURRENT_TIME_UPDATED` - Updated current time of playback.
12 | `VIDEO_EVENTS.DURATION_UPDATED` - Duration of video updated.
13 | 14 | `VIDEO_EVENTS.VOLUME_CHANGED` - Volume changed.
15 | `VIDEO_EVENTS.MUTE_CHANGED` - Video muted or unmuted.
16 | `VIDEO_EVENTS.SOUND_STATE_CHANGED` - Sound state changed. It will emit on both volume changes or muted\unmute.
17 | 18 | `VIDEO_EVENTS.SEEK_IN_PROGRESS` - Triggers when video tag processing seek.
19 | 20 | `VIDEO_EVENTS.UPLOAD_STALLED` - Upload stalled for some reason.
21 | `VIDEO_EVENTS.UPLOAD_SUSPEND` - Upload suspended for some reason.
22 | 23 | `VIDEO_EVENTS.PLAY_REQUEST` - Player was requested for play.
24 | `VIDEO_EVENTS.PLAY_ABORTED` - Player aborted play request.
25 | `VIDEO_EVENTS.RESET` - Triggers when player reseting playback.
26 | -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wix-incubator/playable/d84eb15b309d21577f5ac850b3690ec97920b4d5/docs/favicon.ico -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wix-incubator/playable/d84eb15b309d21577f5ac850b3690ec97920b4d5/docs/logo.png -------------------------------------------------------------------------------- /docs/logo_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wix-incubator/playable/d84eb15b309d21577f5ac850b3690ec97920b4d5/docs/logo_small.png -------------------------------------------------------------------------------- /docs/md-components/PlayableDemo.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import styles from './PlayableDemo.module.scss'; 3 | 4 | const VIDEO_SRC = 5 | 'https://wixmp-e655b44a6e13731b4eedaadd.wixmp.com/Highest Peak.mp4'; 6 | const POSTER_SRC = 'https://wixmp-e655b44a6e13731b4eedaadd.wixmp.com/file.jpg'; 7 | const LOGO_SRC = 8 | 'https://wixmp-e655b44a6e13731b4eedaadd.wixmp.com/White+Wix+logo+Assets+Transparent.png'; 9 | 10 | class PlayableDemo extends PureComponent { 11 | constructor(props) { 12 | super(props); 13 | 14 | this.onRef = this.onRef.bind(this); 15 | } 16 | 17 | onRef(node) { 18 | this.node = node; 19 | } 20 | 21 | componentDidMount() { 22 | this.player = Playable.create({ 23 | src: VIDEO_SRC, 24 | title: 'Playable Demo', 25 | poster: POSTER_SRC, 26 | fillAllSpace: true, 27 | }); 28 | this.player.setLogo(LOGO_SRC); 29 | this.player.showLogo(); 30 | this.player.attachToElement(this.node); 31 | this.player.setFramesMap({ 32 | framesCount: 15, 33 | qualities: [ 34 | { 35 | spriteUrlMask: 36 | 'https://storage.googleapis.com/video-player-media-server-static/demo-thumbnails/low_rez_sprite_%d.jpg', 37 | frameSize: { width: 90, height: 45 }, 38 | framesInSprite: { vert: 10, horz: 10 }, 39 | }, 40 | { 41 | spriteUrlMask: 42 | 'https://storage.googleapis.com/video-player-media-server-static/demo-thumbnails/high_rez_sprite_%d.jpg', 43 | frameSize: { width: 180, height: 90 }, 44 | framesInSprite: { vert: 5, horz: 5 }, 45 | }, 46 | ], 47 | }); 48 | } 49 | 50 | componentWillUnmount() { 51 | this.player.destroy(); 52 | } 53 | 54 | render() { 55 | return
; 56 | } 57 | } 58 | 59 | export default PlayableDemo; 60 | -------------------------------------------------------------------------------- /docs/md-components/PlayableDemo.module.scss: -------------------------------------------------------------------------------- 1 | .playableDemo { 2 | width: 744px; 3 | height: 418px; 4 | } 5 | -------------------------------------------------------------------------------- /docs/md-components/index.js: -------------------------------------------------------------------------------- 1 | import PlayableDemo from './PlayableDemo'; 2 | 3 | export default { 4 | 'playable-demo': PlayableDemo, 5 | }; 6 | -------------------------------------------------------------------------------- /docs/migration.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Player texts" 3 | layout: simple 4 | include: 5 | - ./2.0.0-migration.md 6 | --- 7 | -------------------------------------------------------------------------------- /docs/site.yml: -------------------------------------------------------------------------------- 1 | docsPath: ./docs 2 | distPath: ./sitedist 3 | 4 | mdComponents: 5 | path: ./docs/md-components/index.js 6 | externalScripts: 7 | - https://unpkg.com/playable@2.15.4/dist/statics/playable.bundle.min.js 8 | 9 | # https://www.gatsbyjs.org/docs/gatsby-config 10 | config: 11 | siteMetadata: 12 | title: playable 13 | pathPrefix: /playable 14 | algoliaApiKey: 16e486e2b40c2263303630cc39f2510a 15 | algoliaIndexName: wix_playable 16 | githubLink: https://github.com/wix/playable 17 | 18 | navigation: 19 | - path: / 20 | title: Introduction 21 | - path: /player-config 22 | title: Configuration 23 | - path: /video-source 24 | title: Video source 25 | - path: /api 26 | title: API reference 27 | - path: /events 28 | title: Events 29 | - path: /themes 30 | title: Themes 31 | - path: /modules 32 | title: Modules 33 | - path: /player-texts 34 | title: Custom texts 35 | - path: /adapters 36 | title: Playback adapters 37 | - path: /storybook 38 | title: Storybook 39 | -------------------------------------------------------------------------------- /docs/themes.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Themes" 3 | layout: simple 4 | --- 5 | 6 | # Themes 7 | 8 | We added ability to customize some UI elements. You pass object with theme configuration as second parameret to [Playable.create](/player-config) or you can call method [player.updateTheme](/api#updatetheme) on instance of Playable player. 9 | 10 | ```javascript 11 | const config = { 12 | width: 160, 13 | height: 90 14 | } 15 | 16 | const theme = { 17 | progressColor: "#aaa" 18 | } 19 | 20 | const player = Playable.create(config, theme); 21 | 22 | // ... 23 | 24 | player.updateTheme({ 25 | progressColor: "#faa" 26 | }); 27 | ``` 28 | 29 | Right now we support such parameters: 30 | 31 | ```javascript 32 | theme: { 33 | liveColor: '#ea492e', // color of progress bar in live mode 34 | progressColor: '#fff' // color of progress bar in default mode 35 | } 36 | ``` 37 | 38 | You can play with demo [here](https://jsfiddle.net/OleksiiMakodzeba/xxy5eveb/) 39 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | com.wixpress.media-cloud 5 | playable 6 | pom 7 | playable 8 | playable 9 | 1.0.0-SNAPSHOT 10 | 11 | 12 | com.wixpress.common 13 | wix-master-parent 14 | 100.0.0-SNAPSHOT 15 | 16 | 17 | 18 | 19 | Oleksii Makodzeba 20 | oleksiim@wix.com 21 | 22 | owner 23 | 24 | 25 | 26 | 27 | Videos 28 | 29 | 30 | -------------------------------------------------------------------------------- /scripts/documentation/lib/player/createPlayerApiClassMethod.js: -------------------------------------------------------------------------------- 1 | const { 2 | nodeASTUtils: { cleanUpClassMethod }, 3 | } = require('okidoc-md'); 4 | 5 | const { 6 | isPlayerApiDecorator, 7 | getNameFromPlayerApiDecorator, 8 | } = require('./playerApi'); 9 | 10 | function createPlayerApiClassMethod(node) { 11 | const playerApiDecorator = node.decorators.find(isPlayerApiDecorator); 12 | 13 | cleanUpClassMethod(node, { 14 | identifierName: getNameFromPlayerApiDecorator(playerApiDecorator), 15 | }); 16 | 17 | return node; 18 | } 19 | 20 | module.exports = createPlayerApiClassMethod; 21 | -------------------------------------------------------------------------------- /scripts/documentation/lib/player/createPlayerApiVisitor.js: -------------------------------------------------------------------------------- 1 | const { isPlayerApiDecorator } = require('./playerApi'); 2 | 3 | function createPlayerApiVisitor(enter) { 4 | return { 5 | Decorator(path) { 6 | if (isPlayerApiDecorator(path.node)) { 7 | enter(path.parentPath); 8 | } 9 | }, 10 | }; 11 | } 12 | 13 | module.exports = createPlayerApiVisitor; 14 | -------------------------------------------------------------------------------- /scripts/documentation/lib/player/playerApi.js: -------------------------------------------------------------------------------- 1 | const t = require('@babel/types'); 2 | 3 | function isPlayerApiDecorator(node) { 4 | return t.isDecorator(node) && node.expression.callee.name === 'playerAPI'; 5 | } 6 | 7 | function getNameFromPlayerApiDecorator(decoratorNode) { 8 | const decoratorArguments = decoratorNode.expression.arguments; 9 | 10 | return decoratorArguments.length && t.isStringLiteral(decoratorArguments[0]) 11 | ? decoratorArguments[0].value 12 | : null; 13 | } 14 | 15 | exports.isPlayerApiDecorator = isPlayerApiDecorator; 16 | exports.getNameFromPlayerApiDecorator = getNameFromPlayerApiDecorator; 17 | -------------------------------------------------------------------------------- /scripts/documentation/playerApiVisitor.js: -------------------------------------------------------------------------------- 1 | const createApiVisitor = require('./lib/player/createPlayerApiVisitor'); 2 | const createApiClassMethod = require('./lib/player/createPlayerApiClassMethod'); 3 | 4 | module.exports = { 5 | createApiVisitor, 6 | createApiClassMethod, 7 | }; 8 | -------------------------------------------------------------------------------- /scripts/npm-version.js: -------------------------------------------------------------------------------- 1 | const util = require('util'); 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | const readFile = util.promisify(fs.readFile); 5 | const writeFile = util.promisify(fs.writeFile); 6 | 7 | const packageJSONPath = path.resolve(__dirname, '..', 'package.json'); 8 | const packageJSON = require(packageJSONPath); 9 | 10 | // NOTE: pattern for string like 'https://unpkg.com/playable@1.0.0/dist/statics/playable.bundle.min.js' 11 | const UNPKG_URL_REPLACE_PATTERN = /(https?:\/\/unpkg.com)\/([\w-]+)@(\d+\.\d+\.\d+)\/(.+)/gi; 12 | 13 | function replaceUnpkgVersion(markdownString, version) { 14 | return markdownString.replace( 15 | UNPKG_URL_REPLACE_PATTERN, 16 | `$1/$2@${version}/$4`, 17 | ); 18 | } 19 | 20 | function updateVersionInMarkdown(filePath) { 21 | return readFile(filePath).then(content => 22 | writeFile( 23 | filePath, 24 | replaceUnpkgVersion(content.toString(), packageJSON.version), 25 | ), 26 | ); 27 | } 28 | 29 | const FILES_TO_UPDATE = [ 30 | path.resolve(__dirname, '../README.md'), 31 | path.resolve(__dirname, '../docs/index.md'), 32 | path.resolve(__dirname, '../docs/site.yml'), 33 | ]; 34 | 35 | Promise.all( 36 | FILES_TO_UPDATE.map(filePath => updateVersionInMarkdown(filePath)), 37 | ).catch(error => { 38 | console.error('ERROR: Unable to update version in `md` files', error); 39 | process.exit(1); 40 | }); 41 | -------------------------------------------------------------------------------- /src/constants/engine-state.ts: -------------------------------------------------------------------------------- 1 | enum EngineState { 2 | SRC_SET = 'engine-state/src-set', 3 | LOAD_STARTED = 'engine-state/load-started', 4 | METADATA_LOADED = 'engine-state/metadata-loaded', 5 | READY_TO_PLAY = 'engine-state/ready-to-play', 6 | SEEK_IN_PROGRESS = 'engine-state/seek-in-progress', 7 | PLAY_REQUESTED = 'engine-state/play-requested', 8 | WAITING = 'engine-state/waiting', 9 | PLAYING = 'engine-state/playing', 10 | PAUSED = 'engine-state/paused', 11 | ENDED = 'engine-state/ended', 12 | } 13 | 14 | export default EngineState; 15 | -------------------------------------------------------------------------------- /src/constants/errors.ts: -------------------------------------------------------------------------------- 1 | enum Error { 2 | SRC_PARSE = 'error-src-parse', 3 | MANIFEST_LOAD = 'error-manifest-load', 4 | MANIFEST_PARSE = 'error-manifest-parse', 5 | MANIFEST_INCOMPATIBLE = 'error-manifest-incompatible', 6 | LEVEL_LOAD = 'error-level-load', 7 | CONTENT_LOAD = 'error-content-load', 8 | CONTENT_PARSE = 'error-content-parse', 9 | MEDIA = 'error-media', 10 | UNKNOWN = 'error-unknown', 11 | } 12 | 13 | export default Error; 14 | -------------------------------------------------------------------------------- /src/constants/events/video.ts: -------------------------------------------------------------------------------- 1 | enum VideoEvent { 2 | ERROR = 'video-events/error', 3 | STATE_CHANGED = 'video-events/state-changed', 4 | LIVE_STATE_CHANGED = 'video-events/live-state-changed', 5 | DYNAMIC_CONTENT_ENDED = 'video-events/dynamic-content-ended', 6 | CHUNK_LOADED = 'video-events/chunk-loaded', 7 | CURRENT_TIME_UPDATED = 'video-events/current-time-updated', 8 | DURATION_UPDATED = 'video-events/duration-updated', 9 | SOUND_STATE_CHANGED = 'video-events/sound-state-changed', 10 | VOLUME_CHANGED = 'video-events/volume-changed', 11 | MUTE_CHANGED = 'video-events/mute-changed', 12 | SEEK_IN_PROGRESS = 'video-events/seek-in-progress', 13 | UPLOAD_STALLED = 'video-events/upload-stalled', 14 | UPLOAD_SUSPEND = 'video-events/upload-suspend', 15 | PLAY_REQUEST = 'video-events/play-request', 16 | PLAY_ABORTED = 'video-events/play-aborted', 17 | RESET = 'video-events/reset-playback', 18 | } 19 | 20 | export default VideoEvent; 21 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MediaStreamDeliveryPriority, 3 | MediaStreamType, 4 | MimeToStreamTypeMap, 5 | } from './media-stream'; 6 | import UIEvent from './events/ui'; 7 | import VideoEvent from './events/video'; 8 | import Error from './errors'; 9 | import TextLabel from './text-labels'; 10 | import EngineState from './engine-state'; 11 | import LiveState from './live-state'; 12 | 13 | export { 14 | MediaStreamType, 15 | MediaStreamType as MEDIA_STREAM_TYPES, 16 | MimeToStreamTypeMap, 17 | MediaStreamDeliveryPriority, 18 | MediaStreamDeliveryPriority as MEDIA_STREAM_DELIVERY_PRIORITY, 19 | TextLabel, 20 | TextLabel as TEXT_LABELS, 21 | UIEvent, 22 | UIEvent as UI_EVENTS, 23 | VideoEvent, 24 | VideoEvent as VIDEO_EVENTS, 25 | Error, 26 | Error as ERRORS, 27 | EngineState, 28 | EngineState as ENGINE_STATES, 29 | LiveState, 30 | LiveState as LIVE_STATES, 31 | }; 32 | -------------------------------------------------------------------------------- /src/constants/live-state.ts: -------------------------------------------------------------------------------- 1 | enum LiveState { 2 | NONE = 'live-state/none', 3 | INITIAL = 'live-state/initial', 4 | NOT_SYNC = 'live-state/not-sync', 5 | SYNC = 'live-state/sync', 6 | ENDED = 'live-state/ended', 7 | } 8 | 9 | export default LiveState; 10 | -------------------------------------------------------------------------------- /src/constants/media-stream.ts: -------------------------------------------------------------------------------- 1 | enum MediaStreamType { 2 | MP4 = 'MP4', 3 | WEBM = 'WEBM', 4 | HLS = 'HLS', 5 | DASH = 'DASH', 6 | OGG = 'OGG', 7 | MOV = 'MOV', 8 | MKV = 'MKV', 9 | } 10 | 11 | const MimeToStreamTypeMap: { [mimeType: string]: MediaStreamType } = { 12 | 'application/x-mpegURL': MediaStreamType.HLS, 13 | 'application/vnd.apple.mpegURL': MediaStreamType.HLS, 14 | 'application/dash+xml': MediaStreamType.DASH, 15 | 'video/mp4': MediaStreamType.MP4, 16 | 'video/x-mp4': MediaStreamType.MP4, 17 | 'x-video/mp4': MediaStreamType.MP4, 18 | 'video/webm': MediaStreamType.WEBM, 19 | 'video/ogg': MediaStreamType.OGG, 20 | 'video/quicktime': MediaStreamType.MOV, 21 | 'video/x-matroska': MediaStreamType.MKV, 22 | }; 23 | 24 | enum MediaStreamDeliveryPriority { 25 | NATIVE_PROGRESSIVE, 26 | ADAPTIVE_VIA_MSE, 27 | NATIVE_ADAPTIVE, 28 | FORCED, 29 | } 30 | 31 | export { MediaStreamType, MimeToStreamTypeMap, MediaStreamDeliveryPriority }; 32 | -------------------------------------------------------------------------------- /src/constants/text-labels.ts: -------------------------------------------------------------------------------- 1 | enum TextLabel { 2 | LOGO_LABEL = 'logo-label', 3 | LOGO_TOOLTIP = 'logo-tooltip', 4 | LIVE_INDICATOR_TEXT = 'live-indicator-text', 5 | LIVE_SYNC_LABEL = 'live-sync-button-label', 6 | LIVE_SYNC_TOOLTIP = 'live-sync-button-tooltip', 7 | ENTER_FULL_SCREEN_LABEL = 'enter-full-screen-label', 8 | ENTER_FULL_SCREEN_TOOLTIP = 'enter-full-screen-tooltip', 9 | EXIT_FULL_SCREEN_LABEL = 'exit-full-screen-label', 10 | EXIT_FULL_SCREEN_TOOLTIP = 'exit-full-screen-tooltip', 11 | ENTER_PICTURE_IN_PICTURE_LABEL = 'enter-picture-in-picture-button-label', 12 | ENTER_PICTURE_IN_PICTURE_TOOLTIP = 'enter-picture-in-picture-button-tooltip', 13 | EXIT_PICTURE_IN_PICTURE_LABEL = 'exit-picture-in-picture-button-label', 14 | EXIT_PICTURE_IN_PICTURE_TOOLTIP = 'exit-picture-in-picture-button-tooltip', 15 | PLAY_CONTROL_LABEL = 'play-control-label', 16 | PAUSE_CONTROL_LABEL = 'pause-control-label', 17 | PROGRESS_CONTROL_LABEL = 'progress-control-label', 18 | PROGRESS_CONTROL_VALUE = 'progress-control-value', 19 | UNMUTE_CONTROL_LABEL = 'unmute-control-label', 20 | UNMUTE_CONTROL_TOOLTIP = 'unmute-control-label', 21 | MUTE_CONTROL_LABEL = 'mute-control-label', 22 | MUTE_CONTROL_TOOLTIP = 'mute-control-tooltip', 23 | VOLUME_CONTROL_LABEL = 'volume-control-label', 24 | VOLUME_CONTROL_VALUE = 'volume-control-value', 25 | DOWNLOAD_BUTTON_LABEL = 'download-button-label', 26 | DOWNLOAD_BUTTON_TOOLTIP = 'download-button-tooltip', 27 | } 28 | 29 | export default TextLabel; 30 | -------------------------------------------------------------------------------- /src/core/config.ts: -------------------------------------------------------------------------------- 1 | import { isIOS, isAndroid } from '../utils/device-detection'; 2 | 3 | import { ITextMapConfig } from '../modules/text-map/types'; 4 | import { 5 | PreloadType, 6 | PlayableMediaSource, 7 | CrossOriginValue, 8 | } from '../modules/playback-engine/types'; 9 | 10 | export interface IPlayerConfig { 11 | src?: PlayableMediaSource; 12 | poster?: string; 13 | title?: string; 14 | 15 | width?: number; 16 | height?: number; 17 | fillAllSpace?: boolean; 18 | rtl?: boolean; 19 | 20 | videoElement?: HTMLVideoElement; 21 | 22 | preload?: PreloadType; 23 | autoplay?: boolean; 24 | loop?: boolean; 25 | muted?: boolean; 26 | volume?: number; 27 | playsinline?: boolean; 28 | crossOrigin?: CrossOriginValue; 29 | nativeBrowserControls?: boolean; 30 | preventContextMenu?: boolean; 31 | 32 | disableControlWithClickOnPlayer?: boolean; 33 | disableControlWithKeyboard?: boolean; 34 | 35 | hideMainUI?: boolean; 36 | hideOverlay?: boolean; 37 | 38 | disableFullScreen?: boolean; 39 | 40 | texts?: ITextMapConfig; 41 | } 42 | 43 | const convertUIConfigForIOS = (params: IPlayerConfig): IPlayerConfig => ({ 44 | ...params, 45 | disableControlWithClickOnPlayer: true, 46 | disableControlWithKeyboard: true, 47 | hideMainUI: true, 48 | nativeBrowserControls: true, 49 | }); 50 | 51 | const convertUIConfigForAndroid = (params: IPlayerConfig): IPlayerConfig => ({ 52 | ...params, 53 | disableControlWithClickOnPlayer: true, 54 | disableControlWithKeyboard: true, 55 | }); 56 | 57 | const convertToDeviceRelatedConfig = (params: IPlayerConfig): IPlayerConfig => { 58 | if (isIOS()) { 59 | return convertUIConfigForIOS(params); 60 | } 61 | 62 | if (isAndroid()) { 63 | return convertUIConfigForAndroid(params); 64 | } 65 | 66 | return params; 67 | }; 68 | 69 | export default convertToDeviceRelatedConfig; 70 | -------------------------------------------------------------------------------- /src/core/dependency-container/constants/Lifetime.ts: -------------------------------------------------------------------------------- 1 | enum Lifetime { 2 | SINGLETON = 'singleton', 3 | TRANSIENT = 'transient', 4 | SCOPED = 'scoped', 5 | } 6 | 7 | export default Lifetime; 8 | -------------------------------------------------------------------------------- /src/core/dependency-container/errors/ExtendableError.ts: -------------------------------------------------------------------------------- 1 | export default class ExtendableError extends Error { 2 | constructor(message: string) { 3 | super(message); 4 | 5 | Object.defineProperty(this, 'message', { 6 | enumerable: false, 7 | value: message, 8 | }); 9 | 10 | Object.defineProperty(this, 'name', { 11 | enumerable: false, 12 | value: this.constructor.name, 13 | }); 14 | 15 | Error.captureStackTrace(this, this.constructor); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/core/dependency-container/errors/NotAFunctionError.ts: -------------------------------------------------------------------------------- 1 | import ExtendableError from './ExtendableError'; 2 | 3 | export default class NotAFunctionError extends ExtendableError { 4 | constructor(functionName: string, expectedType: string, givenType: string) { 5 | super( 6 | `The function ${functionName} expected a ${expectedType}, ${givenType} given.`, 7 | ); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/core/dependency-container/errors/ResolutionError.ts: -------------------------------------------------------------------------------- 1 | import ExtendableError from './ExtendableError'; 2 | 3 | const createErrorMessage = ( 4 | name: string, 5 | resolutionStack: string[], 6 | message?: string, 7 | ) => { 8 | resolutionStack = resolutionStack.slice(); 9 | resolutionStack.push(name); 10 | const resolutionPathString = resolutionStack.join(' -> '); 11 | let msg = `Could not resolve '${name}'.`; 12 | if (message) { 13 | msg += ` ${message} \n\n Resolution path: ${resolutionPathString}`; 14 | } 15 | 16 | return msg; 17 | }; 18 | 19 | export default class ResolutionError extends ExtendableError { 20 | constructor(name: string, resolutionStack: string[], message?: string) { 21 | super(createErrorMessage(name, resolutionStack, message)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/core/dependency-container/index.ts: -------------------------------------------------------------------------------- 1 | import createContainer from './createContainer'; 2 | import Lifetime from './constants/Lifetime'; 3 | import registrations from './registrations'; 4 | 5 | export default { 6 | createContainer, 7 | Lifetime, 8 | ...registrations, 9 | }; 10 | -------------------------------------------------------------------------------- /src/core/dependency-container/types.ts: -------------------------------------------------------------------------------- 1 | import Lifetime from './constants/Lifetime'; 2 | 3 | interface IOptions { 4 | lifetime?: Lifetime; 5 | } 6 | 7 | export { IOptions }; 8 | -------------------------------------------------------------------------------- /src/core/dependency-container/utils/nameValueToObject.ts: -------------------------------------------------------------------------------- 1 | import { __assign } from 'tslib'; 2 | 3 | export default function(name: Object | string, value: any): Object { 4 | if (typeof name !== 'object') { 5 | return __assign({ [name]: value }); 6 | } 7 | 8 | return name; 9 | } 10 | -------------------------------------------------------------------------------- /src/core/playable-module.ts: -------------------------------------------------------------------------------- 1 | import { PLAYER_API_PROPERTY } from './player-api-decorator'; 2 | 3 | type ModuleAPI = Record | void; 4 | 5 | // For internal use. Combines 2 possible module API definitions: 6 | // 1) @playerAPI() decorator 7 | // 2) getAPI() method 8 | 9 | export type IModule> = { 10 | [PLAYER_API_PROPERTY]?: Record; 11 | getAPI?(): T; 12 | }; 13 | 14 | // For public use. Omit PLAYER_API_PROPERTY to force consumers to use 'getAPI()'; 15 | 16 | type ModuleWithoutAPI = {}; 17 | 18 | type ModuleWithAPI = ModuleWithoutAPI & 19 | Required, typeof PLAYER_API_PROPERTY>>; 20 | 21 | export type IPlayableModule = T extends void 22 | ? ModuleWithoutAPI 23 | : ModuleWithAPI; 24 | -------------------------------------------------------------------------------- /src/core/player-api-decorator.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import playerAPI, { PLAYER_API_PROPERTY } from './player-api-decorator'; 4 | 5 | describe('Decorator playerAPI', () => { 6 | it('should add method to private property in prototype', () => { 7 | class A { 8 | @playerAPI() 9 | a() {} 10 | 11 | @playerAPI() 12 | b() {} 13 | } 14 | 15 | expect((A as any).prototype[PLAYER_API_PROPERTY]).to.deep.equal({ 16 | a: Reflect.getOwnPropertyDescriptor(A.prototype, 'a'), 17 | b: Reflect.getOwnPropertyDescriptor(A.prototype, 'b'), 18 | }); 19 | }); 20 | 21 | it('should add method to private property in prototype with custom key', () => { 22 | class A { 23 | @playerAPI('b') 24 | a() {} 25 | } 26 | 27 | expect((A as any).prototype[PLAYER_API_PROPERTY]).to.deep.equal({ 28 | b: Reflect.getOwnPropertyDescriptor(A.prototype, 'a'), 29 | }); 30 | }); 31 | 32 | it('should add descriptor if setter or getter', () => { 33 | class A { 34 | @playerAPI('b') 35 | get a() { 36 | return; 37 | } 38 | } 39 | 40 | expect((A as any).prototype[PLAYER_API_PROPERTY]).to.deep.equal({ 41 | b: Reflect.getOwnPropertyDescriptor(A.prototype, 'a'), 42 | }); 43 | }); 44 | 45 | it('should add descriptor if setter and getter', () => { 46 | class A { 47 | @playerAPI() 48 | get a() { 49 | return; 50 | } 51 | 52 | set a(_) {} 53 | } 54 | 55 | expect((A as any).prototype[PLAYER_API_PROPERTY]).to.deep.equal({ 56 | a: Reflect.getOwnPropertyDescriptor(A.prototype, 'a'), 57 | }); 58 | }); 59 | 60 | it('should throw error if same keys for public API', () => { 61 | const getWrongDecoratedClassB = () => { 62 | class B { 63 | @playerAPI('b') 64 | a() {} 65 | 66 | @playerAPI() 67 | b() {} 68 | } 69 | 70 | return B; 71 | }; 72 | 73 | expect(getWrongDecoratedClassB).to.throw( 74 | 'Method "b" for public API in B is already defined', 75 | ); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /src/core/player-api-decorator.ts: -------------------------------------------------------------------------------- 1 | export const PLAYER_API_PROPERTY = '___playerAPI'; 2 | 3 | const checkDescriptorsOnEquality = ( 4 | desc1: PropertyDescriptor, 5 | desc2: PropertyDescriptor, 6 | ) => 7 | desc1.value === desc2.value && 8 | desc1.get === desc2.get && 9 | desc1.set === desc2.set; 10 | 11 | const playerAPI = (name?: string) => ( 12 | target: any, 13 | property: string, 14 | descriptor: PropertyDescriptor, 15 | ) => { 16 | const methodName = name || property; 17 | 18 | if (!target[PLAYER_API_PROPERTY]) { 19 | target[PLAYER_API_PROPERTY] = {}; 20 | } 21 | 22 | if (target[PLAYER_API_PROPERTY][methodName]) { 23 | if ( 24 | !checkDescriptorsOnEquality( 25 | target[PLAYER_API_PROPERTY][methodName], 26 | descriptor, 27 | ) 28 | ) { 29 | throw new Error( 30 | `Method "${methodName}" for public API in ${target.constructor.name} is already defined`, 31 | ); 32 | } 33 | } 34 | 35 | target[PLAYER_API_PROPERTY][methodName] = descriptor; 36 | }; 37 | 38 | export default playerAPI; 39 | -------------------------------------------------------------------------------- /src/core/player-factory.ts: -------------------------------------------------------------------------------- 1 | import DependencyContainer from './dependency-container'; 2 | import PlayerFacade from './player-facade'; 3 | 4 | import defaultModules, { IPlayer } from './default-modules'; 5 | import defaultPlaybackAdapters from '../modules/playback-engine/output/native/adapters/default-set'; 6 | 7 | import { IThemeConfig } from '../modules/ui/core/theme'; 8 | 9 | import { IPlayerConfig } from './config'; 10 | import { IPlaybackAdapterClass } from '../modules/playback-engine/output/native/adapters/types'; 11 | 12 | let additionalModules: { [id: string]: any } = {}; 13 | let playbackAdapters: IPlaybackAdapterClass[] = [...defaultPlaybackAdapters]; 14 | 15 | export const container = DependencyContainer.createContainer(); 16 | container.register(defaultModules); 17 | const defaultModulesNames = Object.keys(defaultModules); 18 | 19 | export function registerModule(id: string, module: any) { 20 | additionalModules[id] = module; 21 | } 22 | 23 | export function registerPlaybackAdapter(adapter: any) { 24 | playbackAdapters.push(adapter); 25 | } 26 | 27 | export function clearAdditionalModules() { 28 | additionalModules = {}; 29 | } 30 | 31 | export function clearPlaybackAdapters() { 32 | playbackAdapters = [...defaultPlaybackAdapters]; 33 | } 34 | 35 | export interface IPlayerInstance extends IPlayer { 36 | destroy(): void; 37 | } 38 | 39 | export function create( 40 | params: IPlayerConfig = {}, 41 | themeConfig?: IThemeConfig, 42 | ): IPlayerInstance { 43 | const scope = container.createScope(); 44 | 45 | const additionalModuleNames = Object.keys(additionalModules); 46 | 47 | if (additionalModuleNames.length) { 48 | additionalModuleNames.forEach(moduleName => 49 | scope.registerClass(moduleName, additionalModules[moduleName], { 50 | lifetime: DependencyContainer.Lifetime.SCOPED, 51 | }), 52 | ); 53 | } 54 | 55 | scope.registerValue('availablePlaybackAdapters', playbackAdapters); 56 | 57 | return new PlayerFacade( 58 | params, 59 | scope, 60 | defaultModulesNames, 61 | additionalModuleNames, 62 | themeConfig, 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /src/e2e/node.js: -------------------------------------------------------------------------------- 1 | // Test that we not using any WebAPI on module initialization 2 | const playable = require('../../dist/statics/playable.bundle'); 3 | console.log('Playable bundle succesfully imported!'); 4 | process.exit(0); 5 | -------------------------------------------------------------------------------- /src/e2e/playback-test.ts: -------------------------------------------------------------------------------- 1 | import Playable from '../index'; 2 | import { NativeEnvironmentSupport } from '../utils/environment-detection'; 3 | 4 | /* ignore coverage */ 5 | describe('Playback e2e test', function() { 6 | this.timeout(10000); 7 | const container = document.createElement('div'); 8 | const formatsToTest = [ 9 | { 10 | type: 'MP4', 11 | url: 12 | 'https://storage.googleapis.com/video-player-media-server-static/sample.mp4', 13 | supportedByEnv: NativeEnvironmentSupport.MP4, 14 | }, 15 | { 16 | type: 'WEBM', 17 | url: 18 | 'https://storage.googleapis.com/video-player-media-server-static/sample.webm', 19 | supportedByEnv: NativeEnvironmentSupport.WEBM, 20 | }, 21 | ]; 22 | 23 | formatsToTest.forEach(formatToTest => { 24 | if (formatToTest.supportedByEnv) { 25 | it(`allows playback of ${formatToTest.type}`, function(done) { 26 | // TODO: describe `@playerApi` methods in `Player` with TS 27 | const player: any = Playable.create(); 28 | player.attachToElement(container); 29 | player.on(Playable.ENGINE_STATES.PLAYING, () => { 30 | player.destroy(); 31 | 32 | done(); 33 | }); 34 | player.setSrc(formatToTest.url); 35 | player.play(); 36 | }); 37 | 38 | it(`allows playback of ${formatToTest.type} when preload = none`, function(done) { 39 | const player: any = Playable.create({ 40 | preload: Playable.PRELOAD_TYPES.NONE, 41 | }); 42 | player.attachToElement(container); 43 | player.on(Playable.ENGINE_STATES.PLAYING, () => { 44 | player.destroy(); 45 | 46 | done(); 47 | }); 48 | player.setSrc(formatToTest.url); 49 | player.play(); 50 | }); 51 | } 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Video Player 6 | 28 | 29 | 30 |
31 |

Switch progress bar color:

32 | 33 | 34 | 35 | 36 |
37 |
38 |
39 |

Switch video format:

40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 |

Switch progress bar mode:

48 | 49 | 50 |
51 | 52 |

Direction:

53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import { button, boolean, color, number, select } from '@storybook/addon-knobs'; 2 | 3 | import { DEFAULT_URLS, MODE_OPTIONS, RGB_HEX } from './stories/constants'; 4 | import { MEDIA_STREAM_TYPES } from './index'; 5 | 6 | import { MediaStreamType } from './constants'; 7 | import { storiesOf } from '@storybook/html'; 8 | import { createPlayerStory } from './stories/createPlayerStory'; 9 | 10 | const videoTypeOptions = Object.keys(DEFAULT_URLS) as MediaStreamType[]; 11 | 12 | const story = storiesOf('Default', module); 13 | 14 | story.add('Default', () => { 15 | const groupDefault = 'Default'; 16 | const groupActions = 'actions'; 17 | 18 | const height = number('height', 350, {}, groupDefault); 19 | const width = number('width', 600, {}, groupDefault); 20 | 21 | const fillAllSpace = boolean('fillAllSpace', false, groupDefault); 22 | const rtl = boolean('rtl', false, groupDefault); 23 | 24 | const videoType = select( 25 | 'videoType', 26 | videoTypeOptions, 27 | MEDIA_STREAM_TYPES.HLS, 28 | groupDefault, 29 | ); 30 | const progressBarMode = select( 31 | 'progressBarMode', 32 | MODE_OPTIONS, 33 | MODE_OPTIONS.REGULAR, 34 | groupDefault, 35 | ); 36 | 37 | const playerColor = color('color', '#fff', 'Default'); 38 | const playerColorHex = playerColor.includes('rgba') 39 | ? `#${RGB_HEX(playerColor).slice(0, -2)}` 40 | : playerColor; 41 | 42 | const props = { 43 | rtl, 44 | fillAllSpace, 45 | width, 46 | height, 47 | videoType, 48 | progressBarMode, 49 | color: playerColorHex, 50 | }; 51 | 52 | const { storyContainer, player } = createPlayerStory('Default', props); 53 | 54 | button('Stop', () => player.pause(), groupActions); 55 | button('Play', () => player.play(), groupActions); 56 | 57 | return storyContainer; 58 | }); 59 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | create, 3 | registerModule, 4 | clearAdditionalModules, 5 | registerPlaybackAdapter, 6 | clearPlaybackAdapters, 7 | IPlayerInstance, 8 | } from './core/player-factory'; 9 | 10 | import { modules as DefaultModules } from './core/default-modules'; 11 | import { Tooltip } from './modules/ui/core/tooltip'; 12 | 13 | import playerAPIDecorator from './core/player-api-decorator'; 14 | 15 | import { 16 | ERRORS, 17 | UI_EVENTS, 18 | VIDEO_EVENTS, 19 | TEXT_LABELS, 20 | MEDIA_STREAM_TYPES, 21 | MEDIA_STREAM_DELIVERY_PRIORITY, 22 | ENGINE_STATES, 23 | LIVE_STATES, 24 | } from './constants'; 25 | import { 26 | PreloadType as PRELOAD_TYPES, 27 | CrossOriginValue as CROSS_ORIGIN_VALUES, 28 | PlayableMediaSource, 29 | } from './modules/playback-engine/types'; 30 | import { VideoViewMode as VIDEO_VIEW_MODES } from './modules/ui/screen/types'; 31 | 32 | import { IPlaybackAdapter } from './modules/playback-engine/output/native/adapters/types'; 33 | import { IThemeConfig } from './modules/ui/core/theme'; 34 | import { IPlayableModule } from './core/playable-module'; 35 | 36 | export { 37 | create, 38 | registerModule, 39 | clearAdditionalModules, 40 | registerPlaybackAdapter, 41 | clearPlaybackAdapters, 42 | ERRORS, 43 | UI_EVENTS, 44 | VIDEO_EVENTS, 45 | TEXT_LABELS, 46 | MEDIA_STREAM_TYPES, 47 | MEDIA_STREAM_DELIVERY_PRIORITY, 48 | ENGINE_STATES, 49 | LIVE_STATES, 50 | VIDEO_VIEW_MODES, 51 | PRELOAD_TYPES, 52 | CROSS_ORIGIN_VALUES, 53 | Tooltip, 54 | playerAPIDecorator, 55 | DefaultModules, 56 | IPlayerInstance, 57 | PlayableMediaSource, 58 | IPlaybackAdapter, 59 | IThemeConfig, 60 | IPlayableModule, 61 | }; 62 | 63 | export default { 64 | create, 65 | registerModule, 66 | clearAdditionalModules, 67 | registerPlaybackAdapter, 68 | clearPlaybackAdapters, 69 | ERRORS, 70 | UI_EVENTS, 71 | VIDEO_EVENTS, 72 | TEXT_LABELS, 73 | MEDIA_STREAM_TYPES, 74 | MEDIA_STREAM_DELIVERY_PRIORITY, 75 | ENGINE_STATES, 76 | LIVE_STATES, 77 | VIDEO_VIEW_MODES, 78 | PRELOAD_TYPES, 79 | CROSS_ORIGIN_VALUES, 80 | Tooltip, 81 | playerAPIDecorator, 82 | DefaultModules, 83 | }; 84 | -------------------------------------------------------------------------------- /src/modules/anomaly-bloodhound/types.ts: -------------------------------------------------------------------------------- 1 | interface ITimeoutMap { 2 | [id: string]: number; 3 | } 4 | 5 | interface IReportType { 6 | id: string; 7 | timeoutTime: number; 8 | } 9 | 10 | interface IReportTypes { 11 | [id: string]: IReportType; 12 | } 13 | 14 | interface IReportReasons { 15 | [id: string]: string; 16 | } 17 | 18 | export { ITimeoutMap, IReportReasons, IReportType, IReportTypes }; 19 | -------------------------------------------------------------------------------- /src/modules/chromecast-manager/types.ts: -------------------------------------------------------------------------------- 1 | interface IChromecastManager { 2 | isCasting: boolean; 3 | isEnabled: boolean; 4 | destroy(): void; 5 | } 6 | 7 | export { IChromecastManager }; 8 | -------------------------------------------------------------------------------- /src/modules/event-emitter/event-emitter.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import { EventEmitter } from 'eventemitter3'; 4 | import { PLAYER_API_PROPERTY } from '../../core/player-api-decorator'; 5 | 6 | import EventEmitterModule from './event-emitter'; 7 | 8 | describe('EventEmitterModule', () => { 9 | let eventEmitter: any; 10 | 11 | beforeEach(() => { 12 | eventEmitter = new EventEmitterModule(); 13 | }); 14 | 15 | it('should return instance of EventEmitter', () => { 16 | expect(eventEmitter instanceof EventEmitter).to.be.true; 17 | }); 18 | 19 | describe("returned instance's destroy", () => { 20 | beforeEach(() => { 21 | eventEmitter.on('EVENT', () => {}); 22 | eventEmitter.on('EVENT2', () => {}); 23 | }); 24 | 25 | it('should remove all listeners for all events', () => { 26 | eventEmitter.destroy(); 27 | expect(eventEmitter.eventNames()).to.be.deep.equal([]); 28 | }); 29 | }); 30 | 31 | describe('public API', () => { 32 | it('should have "on" method', () => { 33 | expect(eventEmitter[PLAYER_API_PROPERTY].on).to.exist; 34 | }); 35 | 36 | it('should have "off" method', () => { 37 | expect(eventEmitter[PLAYER_API_PROPERTY].off).to.exist; 38 | }); 39 | 40 | it('should have "once" method', () => { 41 | expect(eventEmitter[PLAYER_API_PROPERTY].once).to.exist; 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/modules/event-emitter/types.ts: -------------------------------------------------------------------------------- 1 | import { ListenerFn } from 'eventemitter3'; 2 | 3 | interface IEventMap extends Array { 4 | [0]: string; 5 | [1]: ListenerFn; 6 | [2]?: any; 7 | } 8 | 9 | interface IEventEmitter { 10 | on(event: string, fn: ListenerFn, context?: any): this; 11 | off(event: string, fn?: ListenerFn, context?: any, once?: boolean): this; 12 | emitAsync(event: string | symbol, ...args: any[]): Promise | void; 13 | bindEvents(eventsMap: IEventMap[], defaultFnContext?: any): () => void; 14 | destroy(): void; 15 | } 16 | 17 | interface IEventEmitterAPI { 18 | on?(event: string, fn: ListenerFn, context?: any): this; 19 | off?(event: string, fn?: ListenerFn, context?: any, once?: boolean): this; 20 | } 21 | 22 | export { IEventEmitterAPI, IEventEmitter, IEventMap }; 23 | -------------------------------------------------------------------------------- /src/modules/full-screen-manager/types.ts: -------------------------------------------------------------------------------- 1 | interface IFullScreenConfig { 2 | exitFullScreenOnEnd?: boolean; 3 | enterFullScreenOnPlay?: boolean; 4 | exitFullScreenOnPause?: boolean; 5 | pauseVideoOnFullScreenExit?: boolean; 6 | } 7 | 8 | interface IFullScreenManager { 9 | enterFullScreen(): void; 10 | exitFullScreen(): void; 11 | enableExitFullScreenOnPause(): void; 12 | disableExitFullScreenOnPause(): void; 13 | enableExitFullScreenOnEnd(): void; 14 | disableExitFullScreenOnEnd(): void; 15 | enableEnterFullScreenOnPlay(): void; 16 | disableEnterFullScreenOnPlay(): void; 17 | enablePauseVideoOnFullScreenExit(): void; 18 | disablePauseVideoOnFullScreenExit(): void; 19 | isInFullScreen: boolean; 20 | isEnabled: boolean; 21 | destroy(): void; 22 | } 23 | 24 | interface IFullScreenHelper { 25 | isAPIExist: boolean; 26 | isInFullScreen: boolean; 27 | isEnabled: boolean; 28 | request(): void; 29 | exit(): void; 30 | destroy(): void; 31 | } 32 | 33 | interface IFullScreenAPI { 34 | enableExitFullScreenOnPause?(): void; 35 | disableExitFullScreenOnPause?(): void; 36 | enableExitFullScreenOnEnd?(): void; 37 | disableExitFullScreenOnEnd?(): void; 38 | enableEnterFullScreenOnPlay?(): void; 39 | disableEnterFullScreenOnPlay?(): void; 40 | enablePauseVideoOnFullScreenExit?(): void; 41 | disablePauseVideoOnFullScreenExit?(): void; 42 | enterFullScreen?(): void; 43 | exitFullScreen?(): void; 44 | isInFullScreen?: boolean; 45 | } 46 | 47 | export { 48 | IFullScreenAPI, 49 | IFullScreenManager, 50 | IFullScreenHelper, 51 | IFullScreenConfig, 52 | }; 53 | -------------------------------------------------------------------------------- /src/modules/keyboard-control/types.ts: -------------------------------------------------------------------------------- 1 | interface IKeyboardControl { 2 | addKeyControl(key: number, callback: EventListener): void; 3 | destroy(): void; 4 | } 5 | 6 | export { IKeyboardControl }; 7 | -------------------------------------------------------------------------------- /src/modules/picture-in-picture/types.ts: -------------------------------------------------------------------------------- 1 | interface IPictureInPicture { 2 | enterPictureInPicture(): void; 3 | exitPictureInPicture(): void; 4 | disablePictureInPicture(): void; 5 | enablePictureInPicture(): void; 6 | isInPictureInPicture: boolean; 7 | isEnabled: boolean; 8 | destroy(): void; 9 | } 10 | 11 | interface IPictureInPictureHelper { 12 | isInPictureInPicture: boolean; 13 | isEnabled: boolean; 14 | request(): void; 15 | exit(): void; 16 | destroy(): void; 17 | } 18 | 19 | interface IPictureInPictureAPI { 20 | enterPictureInPicture?(): void; 21 | exitPictureInPicture?(): void; 22 | disablePictureInPicture?(): void; 23 | enablePictureInPicture?(): void; 24 | isInPictureInPicture?: boolean; 25 | } 26 | 27 | export { IPictureInPictureAPI, IPictureInPicture, IPictureInPictureHelper }; 28 | -------------------------------------------------------------------------------- /src/modules/playback-engine/output/chromecast/types.ts: -------------------------------------------------------------------------------- 1 | import { IEngineDebugInfo } from '../../types'; 2 | 3 | interface IChromecastDebugInfo extends IEngineDebugInfo { 4 | src: string; 5 | } 6 | 7 | export { IChromecastDebugInfo }; 8 | -------------------------------------------------------------------------------- /src/modules/playback-engine/output/native/adapters/default-set.ts: -------------------------------------------------------------------------------- 1 | import getNativeAdapterCreator from './native'; 2 | import { IPlaybackAdapterClass } from './types'; 3 | import { 4 | MediaStreamType, 5 | MediaStreamDeliveryPriority, 6 | } from '../../../../../constants'; 7 | 8 | const defaultPlaybackAdapters: IPlaybackAdapterClass[] = [ 9 | getNativeAdapterCreator( 10 | MediaStreamType.HLS, 11 | MediaStreamDeliveryPriority.NATIVE_ADAPTIVE, 12 | ), 13 | getNativeAdapterCreator( 14 | MediaStreamType.DASH, 15 | MediaStreamDeliveryPriority.NATIVE_ADAPTIVE, 16 | ), 17 | getNativeAdapterCreator( 18 | MediaStreamType.MP4, 19 | MediaStreamDeliveryPriority.NATIVE_PROGRESSIVE, 20 | ), 21 | getNativeAdapterCreator( 22 | MediaStreamType.WEBM, 23 | MediaStreamDeliveryPriority.NATIVE_PROGRESSIVE, 24 | ), // Native WebM (Chrome, Firefox) 25 | getNativeAdapterCreator( 26 | MediaStreamType.OGG, 27 | MediaStreamDeliveryPriority.NATIVE_PROGRESSIVE, 28 | ), // Native WebM (Chrome, Firefox) 29 | getNativeAdapterCreator( 30 | MediaStreamType.MOV, 31 | MediaStreamDeliveryPriority.NATIVE_PROGRESSIVE, 32 | ), // Native QuickTime .mov (Safari) 33 | getNativeAdapterCreator( 34 | MediaStreamType.MKV, 35 | MediaStreamDeliveryPriority.NATIVE_PROGRESSIVE, 36 | ), 37 | ]; 38 | 39 | export default defaultPlaybackAdapters; 40 | -------------------------------------------------------------------------------- /src/modules/playback-engine/output/native/adapters/types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MediaStreamType, 3 | MediaStreamDeliveryPriority, 4 | } from '../../../../../constants'; 5 | import { IEventEmitter } from '../../../../event-emitter/types'; 6 | import { PlayableMediaSource } from '../../../types'; 7 | 8 | interface IPlaybackAdapter { 9 | canPlay(mediaType: MediaStreamType): boolean; 10 | setMediaStreams(mediaStreams: any): void; 11 | 12 | attach(videoElement: HTMLVideoElement): void; 13 | detach(): void; 14 | 15 | currentUrl: PlayableMediaSource; 16 | 17 | mediaStreamDeliveryPriority: MediaStreamDeliveryPriority; 18 | syncWithLiveTime: number; 19 | isDynamicContent: boolean; 20 | isDynamicContentEnded: boolean; 21 | isSyncWithLive: boolean; 22 | 23 | isSeekAvailable: boolean; 24 | 25 | debugInfo: IAdapterDebugInfo; 26 | } 27 | 28 | interface IPlaybackAdapterClass { 29 | isSupported(): boolean; 30 | new (eventEmitter: IEventEmitter): IPlaybackAdapter; 31 | } 32 | 33 | /** 34 | * @property type - Name of current attached stream. 35 | * @property url - Url of current source 36 | * @property bitrates - List of all available bitrates. Internal structure different for different type of streams 37 | * @property currentBitrate - Current bitrate. Internal structure different for different type of streams 38 | * @property bwEstimate - Estimation of bandwidth 39 | * @property overallBufferLength - Overall length of buffer 40 | * @property nearestBufferSegInfo - Object with start and end for current buffer segment 41 | * @property deliveryPriority - Priority of current adapter 42 | */ 43 | interface IAdapterDebugInfo { 44 | type: MediaStreamType; 45 | url: string; 46 | bitrates: string[]; 47 | currentBitrate: string; 48 | bwEstimate: number; 49 | overallBufferLength: number; 50 | nearestBufferSegInfo: Object; 51 | deliveryPriority: MediaStreamDeliveryPriority; 52 | } 53 | 54 | export { IAdapterDebugInfo, IPlaybackAdapter, IPlaybackAdapterClass }; 55 | -------------------------------------------------------------------------------- /src/modules/playback-engine/playback-engine.spec.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wix-incubator/playable/d84eb15b309d21577f5ac850b3690ec97920b4d5/src/modules/playback-engine/playback-engine.spec.ts -------------------------------------------------------------------------------- /src/modules/playback-engine/utils/adapters-resolver.ts: -------------------------------------------------------------------------------- 1 | import { IPlaybackAdapter } from '../output/native/adapters/types'; 2 | import { MediaStreamType, MimeToStreamTypeMap } from '../../../constants'; 3 | import { IPlayableSource, IParsedPlayableSource } from '../types'; 4 | 5 | export function resolveAdapters( 6 | mediaStreams: IPlayableSource[], 7 | availableAdapters: IPlaybackAdapter[], 8 | ): IPlaybackAdapter[] { 9 | const playableAdapters: IPlaybackAdapter[] = []; 10 | 11 | const groupedStreams = groupStreamsByMediaType(mediaStreams); 12 | const groupedStreamKeys = Object.keys(groupedStreams) as MediaStreamType[]; 13 | 14 | availableAdapters.forEach(adapter => { 15 | for (let i = 0; i < groupedStreamKeys.length; i += 1) { 16 | const mediaType = groupedStreamKeys[i]; 17 | 18 | if (adapter.canPlay(mediaType)) { 19 | adapter.setMediaStreams(groupedStreams[mediaType]); 20 | playableAdapters.push(adapter); 21 | break; 22 | } 23 | } 24 | }); 25 | 26 | playableAdapters.sort( 27 | (firstAdapter, secondAdapter) => 28 | secondAdapter.mediaStreamDeliveryPriority - 29 | firstAdapter.mediaStreamDeliveryPriority, 30 | ); 31 | 32 | return playableAdapters; 33 | } 34 | 35 | function groupStreamsByMediaType(mediaStreams: IPlayableSource[]) { 36 | const typeMap: { [type: string]: IParsedPlayableSource[] } = {}; 37 | mediaStreams.forEach((mediaStream: IPlayableSource) => { 38 | const type: MediaStreamType = 39 | mediaStream.type || MimeToStreamTypeMap[mediaStream.mimeType]; 40 | 41 | if (!type) { 42 | return; 43 | } 44 | 45 | if (!Array.isArray(typeMap[type])) { 46 | typeMap[type] = []; 47 | } 48 | 49 | typeMap[type].push({ 50 | url: mediaStream.url, 51 | type, 52 | }); 53 | }); 54 | 55 | return typeMap; 56 | } 57 | -------------------------------------------------------------------------------- /src/modules/playback-engine/utils/detect-stream-type.spec.ts: -------------------------------------------------------------------------------- 1 | import 'jsdom-global'; 2 | import { expect } from 'chai'; 3 | import { getStreamType } from './detect-stream-type'; 4 | import { MediaStreamType } from '../../../constants'; 5 | 6 | describe('Stream type auto detection', function() { 7 | const testURL = 'http://mocked-domain.com/some/internalPath/'; 8 | const formatsToTest = [ 9 | { type: MediaStreamType.MP4, fileName: 'video.mp4' }, 10 | { type: MediaStreamType.WEBM, fileName: 'video.webm' }, 11 | { type: MediaStreamType.HLS, fileName: 'video.m3u8' }, 12 | { type: MediaStreamType.DASH, fileName: 'video.mpd' }, 13 | ]; 14 | 15 | formatsToTest.forEach(formatToTest => { 16 | it(`should detect ${formatToTest.type} URLs`, function() { 17 | const URL = testURL + formatToTest.fileName; 18 | 19 | expect(getStreamType(URL)).to.equal(formatToTest.type); 20 | }); 21 | }); 22 | 23 | describe('when receive ULR', () => { 24 | const mp4URL = testURL + 'video.mp4'; 25 | const queryParam = '?data=true'; 26 | const fragment = '#sectionOnPage'; 27 | 28 | it('should detect type even if it has query params', () => { 29 | expect(getStreamType(mp4URL + queryParam)).to.equal(MediaStreamType.MP4); 30 | }); 31 | 32 | it('should detect type even if it has fragments', () => { 33 | expect(getStreamType(mp4URL + fragment)).to.equal(MediaStreamType.MP4); 34 | }); 35 | 36 | it('should detect type even if it has fragments and params', () => { 37 | expect(getStreamType(mp4URL + queryParam + fragment)).to.equal( 38 | MediaStreamType.MP4, 39 | ); 40 | }); 41 | }); 42 | 43 | it("should throw error if can't parse url", () => { 44 | expect(getStreamType('test.url')).to.equal(false); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/modules/playback-engine/utils/detect-stream-type.ts: -------------------------------------------------------------------------------- 1 | import { MediaStreamType } from '../../../constants'; 2 | 3 | const extensionsMap = Object.create(null); 4 | extensionsMap.mp4 = MediaStreamType.MP4; 5 | extensionsMap.webm = MediaStreamType.WEBM; 6 | extensionsMap.m3u8 = MediaStreamType.HLS; 7 | extensionsMap.mpd = MediaStreamType.DASH; 8 | extensionsMap.ogg = MediaStreamType.OGG; 9 | extensionsMap.mkv = MediaStreamType.MKV; 10 | extensionsMap.mov = MediaStreamType.MOV; 11 | 12 | export function getStreamType(url: string) { 13 | const anchorElement = document.createElement('a'); 14 | anchorElement.href = url; 15 | const streamType = extensionsMap[getExtFromPath(anchorElement.pathname)]; 16 | 17 | return streamType || false; 18 | } 19 | 20 | export function getExtFromPath(path: string) { 21 | return path 22 | .split('.') 23 | .pop() 24 | .toLowerCase(); 25 | } 26 | -------------------------------------------------------------------------------- /src/modules/root-container/normalize/_button.scss: -------------------------------------------------------------------------------- 1 | // https://github.com/necolas/normalize.css/blob/8.0.0/normalize.css#L147-L215 2 | 3 | /** 4 | * 1. Change the font styles in all browsers. 5 | * 2. Show the overflow in IE. 6 | * 3. Remove the margin in Firefox and Safari. 7 | * 4. Remove the inheritance of text transform in Edge, Firefox, and IE. 8 | * 5. Correct the inability to style clickable types in iOS and Safari. 9 | */ 10 | 11 | button { 12 | font-family: inherit; /* 1 */ 13 | font-size: 100%; /* 1 */ 14 | line-height: 1.15; /* 1 */ 15 | 16 | overflow: visible; /* 2 */ 17 | 18 | margin: 0; /* 3 */ 19 | 20 | text-transform: none; /* 4 */ 21 | 22 | -webkit-appearance: button; /* 5 */ 23 | } 24 | 25 | /** 26 | * Remove the inner border and padding in Firefox. 27 | */ 28 | 29 | button::-moz-focus-inner { 30 | padding: 0; 31 | 32 | border-style: none; 33 | } 34 | 35 | /** 36 | * Restore the focus styles unset by the previous rule. 37 | */ 38 | 39 | button:-moz-focusring { 40 | outline: 1px dotted ButtonText; 41 | } 42 | -------------------------------------------------------------------------------- /src/modules/root-container/normalize/_universal.scss: -------------------------------------------------------------------------------- 1 | &, 2 | *, 3 | *:before, 4 | *:after { 5 | box-sizing: content-box !important; 6 | 7 | outline: none !important; 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/root-container/root-container.scss: -------------------------------------------------------------------------------- 1 | @import '../ui/shared'; 2 | 3 | .container { 4 | font-family: HelveticaNeueW01-45Ligh, HelveticaNeueW02-45Ligh, 5 | HelveticaNeueW10-45Ligh, Helvetica Neue, Helvetica, Arial, 6 | \\30e1\30a4\30ea\30aa, meiryo, \\30d2\30e9\30ae\30ce\89d2\30b4 pro w3, 7 | hiragino kaku gothic pro; 8 | 9 | position: relative; 10 | z-index: 0; 11 | 12 | display: block; 13 | overflow: hidden; 14 | 15 | height: inherit; 16 | 17 | outline: none; 18 | 19 | @import './normalize/button'; 20 | } 21 | 22 | [data-playable-hook='player-container'].container [data-playable-component] { 23 | @import './normalize/universal'; 24 | } 25 | 26 | .fillAllSpace, 27 | .fullScreen { 28 | width: 100% !important; 29 | min-width: 100% !important; 30 | height: 100% !important; 31 | min-height: 100% !important; 32 | } 33 | 34 | :global { 35 | [data-playable-focus-source='key'] [data-playable-hook='player-container'], 36 | [data-playable-focus-source='script'] 37 | [data-playable-hook='player-container'] { 38 | button.focus-within, 39 | input.focus-within, 40 | img.focus-within { 41 | box-shadow: 0 0 0 2px rgba(56, 153, 236, .8); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/modules/root-container/root-container.spec.ts: -------------------------------------------------------------------------------- 1 | import 'jsdom-global/register'; 2 | 3 | import { expect } from 'chai'; 4 | import * as sinon from 'sinon'; 5 | 6 | import EventEmitter from '../event-emitter/event-emitter'; 7 | import RootContainer from './root-container'; 8 | 9 | (global as any).requestAnimationFrame = () => {}; 10 | 11 | describe('RootContainer', () => { 12 | let ui: any = {}; 13 | let eventEmitter: any = {}; 14 | let config: any = {}; 15 | 16 | beforeEach(() => { 17 | config = { 18 | ui: {}, 19 | }; 20 | eventEmitter = new EventEmitter(); 21 | 22 | ui = new RootContainer({ 23 | eventEmitter, 24 | config, 25 | }); 26 | }); 27 | 28 | describe('constructor', () => { 29 | it('should create instance ', () => { 30 | expect(ui).to.exist; 31 | expect(ui.view).to.exist; 32 | }); 33 | }); 34 | 35 | describe('API', () => { 36 | beforeEach(() => { 37 | ui = new RootContainer({ 38 | eventEmitter, 39 | config, 40 | }); 41 | }); 42 | 43 | it('should have method for setting width', () => { 44 | expect(ui.setWidth).to.exist; 45 | }); 46 | 47 | it('should have method for setting height', () => { 48 | expect(ui.setHeight).to.exist; 49 | }); 50 | 51 | it('should have method for setting setFillAllSpace', () => { 52 | sinon.spy(ui.view, 'setFillAllSpaceFlag'); 53 | ui.setFillAllSpace(true); 54 | expect(ui.view.setFillAllSpaceFlag.calledWith(true)).to.be.true; 55 | }); 56 | 57 | it('should have method for showing whole view', () => { 58 | expect(ui.show).to.exist; 59 | ui.show(); 60 | expect(ui.isHidden).to.be.false; 61 | }); 62 | 63 | it('should have method for hiding whole view', () => { 64 | expect(ui.hide).to.exist; 65 | ui.hide(); 66 | expect(ui.isHidden).to.be.true; 67 | }); 68 | 69 | it('should have method for destroy', () => { 70 | expect(ui.destroy).to.exist; 71 | ui.destroy(); 72 | }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /src/modules/root-container/templates/container.dot: -------------------------------------------------------------------------------- 1 |
7 |
8 | -------------------------------------------------------------------------------- /src/modules/root-container/templates/index.ts: -------------------------------------------------------------------------------- 1 | import template from './container.dot'; 2 | const containerTemplate = template.default ? template.default : template; 3 | export { containerTemplate }; 4 | -------------------------------------------------------------------------------- /src/modules/root-container/types.ts: -------------------------------------------------------------------------------- 1 | type IRootContainerViewStyles = { 2 | container: string; 3 | fillAllSpace: string; 4 | fullScreen: string; 5 | hidden: string; 6 | rtl: boolean; 7 | }; 8 | 9 | type IRootContainerViewCallbacks = { 10 | onMouseEnter: EventListener; 11 | onMouseMove: EventListener; 12 | onMouseLeave: EventListener; 13 | }; 14 | 15 | type IRootContainerViewConfig = { 16 | width: number; 17 | height: number; 18 | fillAllSpace: boolean; 19 | callbacks: IRootContainerViewCallbacks; 20 | rtl: boolean; 21 | }; 22 | 23 | interface IRootContainer { 24 | getElement(): HTMLElement; 25 | appendComponentElement(element: HTMLElement): void; 26 | attachToElement(element: HTMLElement): void; 27 | setWidth(width: number): void; 28 | getWidth(): number; 29 | setHeight(height: number): void; 30 | getHeight(): number; 31 | setRtl(rtl: boolean): void; 32 | setFillAllSpace(flag: boolean): void; 33 | hide(): void; 34 | show(): void; 35 | destroy(): void; 36 | } 37 | 38 | interface IRootContainerAPI { 39 | getElement?(): HTMLElement; 40 | attachToElement?(element: HTMLElement): void; 41 | setWidth?(width: number): void; 42 | getWidth?(): number; 43 | setHeight?(height: number): void; 44 | getHeight?(): number; 45 | setFillAllSpace?(flag: boolean): void; 46 | setRtl?(rtl: boolean): void; 47 | hide?(): void; 48 | show?(): void; 49 | } 50 | 51 | export { 52 | IRootContainerAPI, 53 | IRootContainer, 54 | IRootContainerViewStyles, 55 | IRootContainerViewCallbacks, 56 | IRootContainerViewConfig, 57 | }; 58 | -------------------------------------------------------------------------------- /src/modules/text-map/default-texts.ts: -------------------------------------------------------------------------------- 1 | import { TextLabel } from '../../constants'; 2 | import { ITextMapConfig } from './types'; 3 | 4 | const map: ITextMapConfig = { 5 | [TextLabel.LOGO_LABEL]: 'Watch on site', 6 | [TextLabel.LOGO_TOOLTIP]: 'Watch On Site', 7 | [TextLabel.LIVE_INDICATOR_TEXT]: ({ isEnded }) => 8 | !isEnded ? 'Live' : 'Live Ended', 9 | [TextLabel.LIVE_SYNC_LABEL]: 'Sync to live', 10 | [TextLabel.LIVE_SYNC_TOOLTIP]: 'Sync to Live', 11 | [TextLabel.PAUSE_CONTROL_LABEL]: 'Pause', 12 | [TextLabel.PLAY_CONTROL_LABEL]: 'Play', 13 | [TextLabel.PROGRESS_CONTROL_LABEL]: 'Progress control', 14 | [TextLabel.PROGRESS_CONTROL_VALUE]: ({ percent }) => 15 | `Already played ${percent}%`, 16 | [TextLabel.MUTE_CONTROL_LABEL]: 'Mute', 17 | [TextLabel.MUTE_CONTROL_TOOLTIP]: 'Mute', 18 | [TextLabel.UNMUTE_CONTROL_LABEL]: 'Unmute', 19 | [TextLabel.UNMUTE_CONTROL_TOOLTIP]: 'Unmute', 20 | [TextLabel.VOLUME_CONTROL_LABEL]: 'Volume control', 21 | [TextLabel.VOLUME_CONTROL_VALUE]: ({ volume }) => `Volume is ${volume}%`, 22 | [TextLabel.ENTER_FULL_SCREEN_LABEL]: 'Enter full screen', 23 | [TextLabel.ENTER_FULL_SCREEN_TOOLTIP]: 'Enter Full Screen', 24 | [TextLabel.EXIT_FULL_SCREEN_LABEL]: 'Exit full screen', 25 | [TextLabel.EXIT_FULL_SCREEN_TOOLTIP]: 'Exit Full Screen', 26 | [TextLabel.ENTER_PICTURE_IN_PICTURE_LABEL]: 'Play Picture-in-Picture', 27 | [TextLabel.ENTER_PICTURE_IN_PICTURE_TOOLTIP]: 'Play Picture-in-Picture', 28 | [TextLabel.EXIT_PICTURE_IN_PICTURE_LABEL]: 'Exit Picture-in-Picture', 29 | [TextLabel.EXIT_PICTURE_IN_PICTURE_TOOLTIP]: 'Exit Picture-in-Picture', 30 | [TextLabel.DOWNLOAD_BUTTON_LABEL]: 'Download video', 31 | [TextLabel.DOWNLOAD_BUTTON_TOOLTIP]: 'Download Video', 32 | }; 33 | 34 | export default map; 35 | -------------------------------------------------------------------------------- /src/modules/text-map/text-map.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import TextMap from './text-map'; 4 | 5 | import createPlayerTestkit from '../../testkit'; 6 | 7 | describe('TextMap module', () => { 8 | let testkit: any; 9 | 10 | beforeEach(() => { 11 | testkit = createPlayerTestkit(); 12 | testkit.registerModule('textMap', TextMap); 13 | }); 14 | 15 | it('should have ability to get text from it', () => { 16 | testkit.setConfig({ 17 | texts: { 18 | testID: 'testText', 19 | }, 20 | }); 21 | const map = testkit.getModule('textMap'); 22 | expect(map.get).to.exist; 23 | expect(map.get('testID')).to.be.equal('testText'); 24 | }); 25 | 26 | it('should pass arguments to translate function', () => { 27 | testkit.setConfig({ 28 | texts: { 29 | testID: ({ arg }: { arg: any }) => `Test:${arg}`, 30 | }, 31 | }); 32 | 33 | const map = testkit.getModule('textMap'); 34 | expect(map.get('testID', { arg: 1 })).to.be.equal('Test:1'); 35 | }); 36 | 37 | it('should return undefined if destroyed', () => { 38 | testkit.setConfig({ 39 | texts: { 40 | testID: 'testText', 41 | }, 42 | }); 43 | const map = testkit.getModule('textMap'); 44 | map.destroy(); 45 | expect(map.get('testID')).to.be.equal(undefined); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /src/modules/text-map/text-map.ts: -------------------------------------------------------------------------------- 1 | import DEFAULT_TEXTS from './default-texts'; 2 | 3 | import { ITextMap, ITextMapConfig } from './types'; 4 | import { IPlayerConfig } from '../../core/config'; 5 | 6 | export default class TextMap implements ITextMap { 7 | static moduleName = 'textMap'; 8 | static dependencies = ['config']; 9 | 10 | private _textMap: ITextMapConfig; 11 | 12 | constructor({ config }: { config: IPlayerConfig }) { 13 | this._textMap = { 14 | ...DEFAULT_TEXTS, 15 | ...config.texts, 16 | }; 17 | } 18 | 19 | get(id: string, args: any, defaultText?: string | Function): string { 20 | if (!this._textMap) { 21 | return; 22 | } 23 | 24 | const text = this._textMap[id] || defaultText; 25 | 26 | if (typeof text === 'function') { 27 | return text(args); 28 | } 29 | 30 | return text; 31 | } 32 | 33 | destroy() { 34 | this._textMap = null; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/modules/text-map/types.ts: -------------------------------------------------------------------------------- 1 | type TextResolver = (args: any) => string; 2 | 3 | interface ITextMapConfig { 4 | [index: string]: string | TextResolver; 5 | } 6 | 7 | interface ITextMap { 8 | get(id: string, args?: any, defaultText?: string | Function): string; 9 | destroy(): void; 10 | } 11 | 12 | export { TextResolver, ITextMap, ITextMapConfig }; 13 | -------------------------------------------------------------------------------- /src/modules/ui/bottom-block/templates/bottom-block.dot: -------------------------------------------------------------------------------- 1 |
3 |
5 |
6 |
8 |
9 |
10 |
12 |
14 |
15 |
17 |
18 |
20 |
21 |
22 |
24 |
26 |
27 |
29 |
30 |
32 |
33 |
34 |
36 |
37 |
38 |
39 | -------------------------------------------------------------------------------- /src/modules/ui/bottom-block/templates/index.ts: -------------------------------------------------------------------------------- 1 | import template from './bottom-block.dot'; 2 | const bottomBlockTemplate = template.default ? template.default : template; 3 | export { bottomBlockTemplate }; 4 | -------------------------------------------------------------------------------- /src/modules/ui/conditions.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * The challenge here to support "playable queries" and "direction" at the same time and allow mixins like: 3 | * @include query(max-width-550()) 4 | * @include query(max-width-550(), ltr()) 5 | * @include query(max-width-550(), rtl()) 6 | */ 7 | 8 | @function max-width-query($value) { 9 | @return '[data-playable-max-width~="#{$value}"]'; 10 | } 11 | 12 | @function direction-query($direction) { 13 | @return '[data-playable-dir="#{$direction}"]'; 14 | } 15 | 16 | @mixin query($mixins...) { 17 | $query: ''; 18 | 19 | @each $mixin in $mixins { 20 | $query: $query + $mixin; 21 | } 22 | 23 | [data-playable-hook='player-container']#{$query} & { 24 | @content; 25 | } 26 | } 27 | 28 | @function in-full-screen() { 29 | @return '[data-playable-in-full-screen="true"]'; 30 | } 31 | 32 | @function max-width-550() { 33 | @return max-width-query(550px); 34 | } 35 | 36 | @function max-width-400() { 37 | @return max-width-query(400px); 38 | } 39 | 40 | @function max-width-350() { 41 | @return max-width-query(350px); 42 | } 43 | 44 | @function max-width-300() { 45 | @return max-width-query(300px); 46 | } 47 | 48 | @function max-width-280() { 49 | @return max-width-query(280px); 50 | } 51 | 52 | @function ltr() { 53 | @return direction-query('ltr'); 54 | } 55 | 56 | @function rtl() { 57 | @return direction-query('rtl'); 58 | } 59 | -------------------------------------------------------------------------------- /src/modules/ui/controls/chromecast/chromecast.scss: -------------------------------------------------------------------------------- 1 | @import '../../shared'; 2 | 3 | .buttonWrapper { 4 | position: relative; 5 | 6 | display: flex; 7 | 8 | align-items: center; 9 | justify-content: center; 10 | } 11 | 12 | .chromecastButton { 13 | width: 18px; 14 | height: 19px; 15 | padding-right: 5px; 16 | 17 | cursor: pointer; 18 | transition: transform .2s; 19 | 20 | @include query(in-full-screen()) { 21 | width: 27px; 22 | height: 28px; 23 | padding-top: 2px; 24 | } 25 | 26 | &:hover { 27 | transform: scale(1.18); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/modules/ui/controls/chromecast/chromecast.theme.ts: -------------------------------------------------------------------------------- 1 | import { IThemeConfig } from '../../core/theme/types'; 2 | 3 | export default { 4 | chromecastButtonFill: { 5 | '--disconnected-color': (data: IThemeConfig) => data.color, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /src/modules/ui/controls/chromecast/templates/chromecast.dot: -------------------------------------------------------------------------------- 1 |
2 | 5 |
6 | -------------------------------------------------------------------------------- /src/modules/ui/controls/chromecast/templates/index.ts: -------------------------------------------------------------------------------- 1 | import template from './chromecast.dot'; 2 | const buttonTemplate = template.default ? template.default : template; 3 | export { buttonTemplate }; 4 | -------------------------------------------------------------------------------- /src/modules/ui/controls/chromecast/types.ts: -------------------------------------------------------------------------------- 1 | import { IThemeService } from '../../core/theme'; 2 | import { ITooltipService } from '../../core/tooltip/types'; 3 | import { ITextMap } from '../../../text-map/types'; 4 | 5 | type IChromecastStyles = { 6 | downloadButton: string; 7 | buttonWrapper: string; 8 | hidden: string; 9 | }; 10 | 11 | type IChromecastViewCallbacks = { 12 | onButtonClick: Function; 13 | }; 14 | 15 | type IChromecastViewConfig = { 16 | callbacks: IChromecastViewCallbacks; 17 | textMap: ITextMap; 18 | theme: IThemeService; 19 | tooltipService: ITooltipService; 20 | }; 21 | 22 | interface IChromecastButton { 23 | getElement(): HTMLElement; 24 | 25 | setChromecastButtonCallback(callback: Function): void; 26 | 27 | show(): void; 28 | hide(): void; 29 | 30 | destroy(): void; 31 | } 32 | 33 | export { 34 | IChromecastButton, 35 | IChromecastStyles, 36 | IChromecastViewCallbacks, 37 | IChromecastViewConfig, 38 | }; 39 | -------------------------------------------------------------------------------- /src/modules/ui/controls/download/download.scss: -------------------------------------------------------------------------------- 1 | @import '../../shared'; 2 | 3 | .buttonWrapper { 4 | position: relative; 5 | 6 | display: flex; 7 | 8 | align-items: center; 9 | justify-content: center; 10 | } 11 | 12 | .downloadButton { 13 | width: 26px; 14 | min-width: 26px; 15 | height: 26px; 16 | min-height: 26px; 17 | 18 | transition: transform .2s; 19 | 20 | @include query(in-full-screen()) { 21 | width: 35px; 22 | min-width: 35px; 23 | height: 35px; 24 | min-height: 21px; 25 | 26 | .icon_small { 27 | display: none; 28 | } 29 | 30 | .icon_big { 31 | display: block; 32 | } 33 | } 34 | 35 | .icon_small { 36 | display: block; 37 | } 38 | 39 | .icon_big { 40 | display: none; 41 | } 42 | 43 | &:hover { 44 | transform: scale(1.18); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/modules/ui/controls/download/download.theme.ts: -------------------------------------------------------------------------------- 1 | import { IThemeConfig } from '../../core/theme/types'; 2 | 3 | export default { 4 | downloadSvgFill: { 5 | fill: (data: IThemeConfig) => data.color, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /src/modules/ui/controls/download/templates/control.dot: -------------------------------------------------------------------------------- 1 |
2 | 13 |
14 | -------------------------------------------------------------------------------- /src/modules/ui/controls/download/templates/index.ts: -------------------------------------------------------------------------------- 1 | import template from './control.dot'; 2 | const controlTemplate = template.default ? template.default : template; 3 | export { controlTemplate }; 4 | -------------------------------------------------------------------------------- /src/modules/ui/controls/download/types.ts: -------------------------------------------------------------------------------- 1 | import { IThemeService } from '../../core/theme'; 2 | import { ITooltipService } from '../../core/tooltip/types'; 3 | import { ITextMap } from '../../../text-map/types'; 4 | 5 | type IDownloadViewStyles = { 6 | downloadButton: string; 7 | buttonWrapper: string; 8 | hidden: string; 9 | }; 10 | 11 | type IDownloadViewCallbacks = { 12 | onButtonClick: () => void; 13 | }; 14 | 15 | type IDownloadViewConfig = { 16 | callbacks: IDownloadViewCallbacks; 17 | textMap: ITextMap; 18 | theme: IThemeService; 19 | tooltipService: ITooltipService; 20 | }; 21 | 22 | interface IDownloadButton { 23 | getElement(): HTMLElement; 24 | 25 | setDownloadClickCallback(callback: () => void): void; 26 | 27 | show(): void; 28 | hide(): void; 29 | 30 | destroy(): void; 31 | } 32 | 33 | interface IDownloadButtonAPI { 34 | setDownloadClickCallback?(callback: () => void): void; 35 | } 36 | 37 | export { 38 | IDownloadButtonAPI, 39 | IDownloadButton, 40 | IDownloadViewStyles, 41 | IDownloadViewCallbacks, 42 | IDownloadViewConfig, 43 | }; 44 | -------------------------------------------------------------------------------- /src/modules/ui/controls/full-screen/full-screen.scss: -------------------------------------------------------------------------------- 1 | @import '../../shared'; 2 | 3 | .fullScreenControl { 4 | position: relative; 5 | 6 | display: flex; 7 | 8 | align-items: center; 9 | justify-content: center; 10 | } 11 | 12 | .fullScreenToggle { 13 | width: 26px; 14 | min-width: 26px; 15 | height: 26px; 16 | min-height: 26px; 17 | 18 | transition: transform .2s; 19 | 20 | @include query(in-full-screen()) { 21 | width: 35px; 22 | height: 35px; 23 | } 24 | 25 | &:hover { 26 | transform: scale(1.18); 27 | } 28 | 29 | .enterIcon { 30 | display: block; 31 | } 32 | 33 | .exitIcon { 34 | display: none; 35 | } 36 | 37 | &.inFullScreen { 38 | &:hover { 39 | transform: scale(.8); 40 | } 41 | 42 | .enterIcon { 43 | display: none; 44 | } 45 | 46 | .exitIcon { 47 | display: block; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/modules/ui/controls/full-screen/full-screen.theme.ts: -------------------------------------------------------------------------------- 1 | import { IThemeConfig } from '../../core/theme/types'; 2 | 3 | export default { 4 | fullScreenSvgFill: { 5 | fill: (data: IThemeConfig) => data.color, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /src/modules/ui/controls/full-screen/templates/control.dot: -------------------------------------------------------------------------------- 1 |
3 | 15 |
16 | -------------------------------------------------------------------------------- /src/modules/ui/controls/full-screen/templates/index.ts: -------------------------------------------------------------------------------- 1 | import template from './control.dot'; 2 | const controlTemplate = template.default ? template.default : template; 3 | export { controlTemplate }; 4 | -------------------------------------------------------------------------------- /src/modules/ui/controls/full-screen/types.ts: -------------------------------------------------------------------------------- 1 | import { IThemeService } from '../../core/theme'; 2 | import { ITooltipService } from '../../core/tooltip/types'; 3 | import { ITextMap } from '../../../text-map/types'; 4 | 5 | type IFullScreenViewStyles = { 6 | fullScreenControl: string; 7 | fullScreenToggle: string; 8 | enterIcon: string; 9 | exitIcon: string; 10 | icon: string; 11 | inFullScreen: string; 12 | hidden: string; 13 | }; 14 | 15 | type IFullScreenViewCallbacks = { 16 | onButtonClick(): void; 17 | }; 18 | 19 | type IFullScreenViewConfig = { 20 | callbacks: IFullScreenViewCallbacks; 21 | textMap: ITextMap; 22 | theme: IThemeService; 23 | tooltipService: ITooltipService; 24 | }; 25 | 26 | interface IFullScreenControl { 27 | getElement(): HTMLElement; 28 | 29 | show(): void; 30 | hide(): void; 31 | 32 | destroy(): void; 33 | } 34 | 35 | export { 36 | IFullScreenControl, 37 | IFullScreenViewStyles, 38 | IFullScreenViewCallbacks, 39 | IFullScreenViewConfig, 40 | }; 41 | -------------------------------------------------------------------------------- /src/modules/ui/controls/logo/logo.scss: -------------------------------------------------------------------------------- 1 | @import '../../conditions'; 2 | @import '../../shared'; 3 | 4 | .logoWrapper { 5 | position: relative; 6 | z-index: 3; 7 | 8 | display: flex; 9 | 10 | transition: opacity .2s; 11 | transition-duration: .2s; 12 | 13 | opacity: 1; 14 | 15 | justify-content: center; 16 | align-items: center; 17 | 18 | &:hover { 19 | .logoPlaceholder { 20 | opacity: .7; 21 | } 22 | } 23 | } 24 | 25 | .logoImage { 26 | max-width: 125px; 27 | max-height: 26px; 28 | 29 | transition: opacity .2s; 30 | 31 | @include query(max-width-550()) { 32 | max-width: 90px; 33 | max-height: 20px; 34 | } 35 | @include query(max-width-350()) { 36 | max-width: 70px; 37 | max-height: 18px; 38 | } 39 | @include query(in-full-screen()) { 40 | max-width: 450px; 41 | max-height: 36px; 42 | } 43 | &.hidden { 44 | //for accessibility reasons 45 | display: none; 46 | } 47 | } 48 | 49 | .logoButton { 50 | width: 26px; 51 | min-width: 26px; 52 | height: 26px; 53 | min-height: 26px; 54 | 55 | transition: transform .2s; 56 | 57 | @include query(in-full-screen()) { 58 | width: 35px; 59 | min-width: 35px; 60 | height: 35px; 61 | min-height: 35px; 62 | 63 | .icon_small { 64 | display: none; 65 | } 66 | 67 | .icon_big { 68 | display: block; 69 | } 70 | } 71 | 72 | .icon_small { 73 | display: block; 74 | } 75 | 76 | .icon_big { 77 | display: none; 78 | } 79 | 80 | &:hover { 81 | transform: scale(1.2); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/modules/ui/controls/logo/logo.theme.ts: -------------------------------------------------------------------------------- 1 | import { IThemeConfig } from '../../core/theme/types'; 2 | 3 | export default { 4 | logoButtonSvgFill: { 5 | fill: (data: IThemeConfig) => data.color, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /src/modules/ui/controls/logo/templates/index.ts: -------------------------------------------------------------------------------- 1 | import template1 from './logo.dot'; 2 | import template2 from './logo-image.dot'; 3 | import template3 from './logo-input.dot'; 4 | import template4 from './logo-button.dot'; 5 | 6 | const logoTemplate = template1.default ? template1.default : template1; 7 | const logoImageTemplate = template2.default ? template2.default : template2; 8 | const logoInputTemplate = template3.default ? template3.default : template3; 9 | const logoButtonTemplate = template4.default ? template4.default : template4; 10 | 11 | export { 12 | logoTemplate, 13 | logoImageTemplate, 14 | logoButtonTemplate, 15 | logoInputTemplate, 16 | }; 17 | -------------------------------------------------------------------------------- /src/modules/ui/controls/logo/templates/logo-button.dot: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /src/modules/ui/controls/logo/templates/logo-image.dot: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /src/modules/ui/controls/logo/templates/logo-input.dot: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /src/modules/ui/controls/logo/templates/logo.dot: -------------------------------------------------------------------------------- 1 |
2 |
3 | -------------------------------------------------------------------------------- /src/modules/ui/controls/logo/types.ts: -------------------------------------------------------------------------------- 1 | import { ITooltipService } from '../../core/tooltip/types'; 2 | import { ITextMap } from '../../../text-map/types'; 3 | import { IThemeService } from '../../core/theme'; 4 | 5 | type ILogoViewStyles = { 6 | logoWrapper: string; 7 | logoButton: string; 8 | logoImage: string; 9 | hidden: string; 10 | }; 11 | 12 | type ILogoViewCallbacks = { 13 | onLogoClick: () => void; 14 | }; 15 | 16 | type ILogoViewConfig = { 17 | theme: IThemeService; 18 | callbacks: ILogoViewCallbacks; 19 | textMap: ITextMap; 20 | tooltipService: ITooltipService; 21 | logo?: string; 22 | }; 23 | 24 | interface ILogoControl { 25 | getElement(): HTMLElement; 26 | 27 | setLogo(src: string): void; 28 | setLogoClickCallback(callback?: () => void): void; 29 | 30 | show(): void; 31 | hide(): void; 32 | 33 | destroy(): void; 34 | } 35 | 36 | interface ILogoAPI { 37 | setLogo?(src: string): void; 38 | setLogoClickCallback?(callback?: () => void): void; 39 | } 40 | 41 | export { 42 | ILogoAPI, 43 | ILogoControl, 44 | ILogoViewStyles, 45 | ILogoViewConfig, 46 | ILogoViewCallbacks, 47 | }; 48 | -------------------------------------------------------------------------------- /src/modules/ui/controls/picture-in-picture/picture-in-picture.scss: -------------------------------------------------------------------------------- 1 | @import '../../shared'; 2 | 3 | .pictureInPictureControl { 4 | position: relative; 5 | 6 | display: flex; 7 | 8 | align-items: center; 9 | justify-content: center; 10 | } 11 | 12 | .pictureInPictureToggle { 13 | width: 26px; 14 | min-width: 26px; 15 | height: 26px; 16 | min-height: 26px; 17 | 18 | transition: transform .2s; 19 | 20 | @include query(in-full-screen()) { 21 | width: 35px; 22 | min-width: 35px; 23 | height: 35px; 24 | min-height: 21px; 25 | 26 | .icon_small { 27 | display: none; 28 | } 29 | 30 | .icon_big { 31 | display: block; 32 | } 33 | } 34 | 35 | .enterIcon { 36 | display: block; 37 | } 38 | 39 | .exitIcon { 40 | display: none; 41 | } 42 | 43 | .icon_small { 44 | display: block; 45 | } 46 | 47 | .icon_big { 48 | display: none; 49 | } 50 | 51 | &:hover { 52 | transform: scale(1.18); 53 | } 54 | 55 | &.inPictureInPicture { 56 | &:hover { 57 | transform: scale(.8); 58 | } 59 | 60 | .enterIcon { 61 | display: none; 62 | } 63 | 64 | .exitIcon { 65 | display: block; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/modules/ui/controls/picture-in-picture/picture-in-picture.spec.ts: -------------------------------------------------------------------------------- 1 | import 'jsdom-global/register'; 2 | import { expect } from 'chai'; 3 | import * as sinon from 'sinon'; 4 | 5 | import createPlayerTestkit from '../../../../testkit'; 6 | 7 | import { UIEvent } from '../../../../constants'; 8 | 9 | class PictureInPictureMock { 10 | enterPictureInPicture = function() {}; 11 | exitPictureInPicture = function() {}; 12 | isEnabled = true; 13 | _config: Object = {}; 14 | } 15 | 16 | describe('PictureInPictureControl', () => { 17 | let testkit; 18 | let control: any = {}; 19 | let eventEmitter: any = {}; 20 | 21 | beforeEach(() => { 22 | testkit = createPlayerTestkit(); 23 | testkit.registerModule('pictureInPicture', PictureInPictureMock); 24 | eventEmitter = testkit.getModule('eventEmitter'); 25 | control = testkit.getModule('pictureInPictureControl'); 26 | }); 27 | 28 | describe('constructor', () => { 29 | it('should create instance ', () => { 30 | expect(control).to.exist; 31 | expect(control.view).to.exist; 32 | }); 33 | }); 34 | 35 | describe('ui events listeners', () => { 36 | it('should call callback on playback state change', async function() { 37 | const spy = sinon.spy(control.view, 'setPictureInPictureState'); 38 | control._bindEvents(); 39 | await eventEmitter.emitAsync(UIEvent.PICTURE_IN_PICTURE_STATUS_CHANGE); 40 | expect(spy.called).to.be.true; 41 | }); 42 | }); 43 | 44 | describe('API', () => { 45 | it('should have method for showing whole view', () => { 46 | expect(control.show).to.exist; 47 | control.show(); 48 | expect(control.isHidden).to.be.false; 49 | }); 50 | 51 | it('should have method for hiding whole view', () => { 52 | expect(control.hide).to.exist; 53 | control.hide(); 54 | expect(control.isHidden).to.be.true; 55 | }); 56 | 57 | it('should have method for destroying', () => { 58 | const spy = sinon.spy(control, '_unbindEvents'); 59 | expect(control.destroy).to.exist; 60 | control.destroy(); 61 | expect(spy.called).to.be.true; 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /src/modules/ui/controls/picture-in-picture/picture-in-picture.theme.ts: -------------------------------------------------------------------------------- 1 | import { IThemeConfig } from '../../core/theme/types'; 2 | 3 | export default { 4 | pictureInPictureSvgFill: { 5 | fill: (data: IThemeConfig) => data.color, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /src/modules/ui/controls/picture-in-picture/templates/index.ts: -------------------------------------------------------------------------------- 1 | import template from './control.dot'; 2 | const controlTemplate = template.default ? template.default : template; 3 | export { controlTemplate }; 4 | -------------------------------------------------------------------------------- /src/modules/ui/controls/picture-in-picture/types.ts: -------------------------------------------------------------------------------- 1 | import { IThemeService } from '../../core/theme'; 2 | import { ITooltipService } from '../../core/tooltip/types'; 3 | import { ITextMap } from '../../../text-map/types'; 4 | 5 | type IPictureInPictureViewStyles = { 6 | pictureInPictureControl: string; 7 | pictureInPictureToggle: string; 8 | icon: string; 9 | inPictureInPicture: string; 10 | hidden: string; 11 | }; 12 | 13 | type IPictureInPictureViewCallbacks = { 14 | onButtonClick(): void; 15 | }; 16 | 17 | type IPictureInPictureViewConfig = { 18 | callbacks: IPictureInPictureViewCallbacks; 19 | textMap: ITextMap; 20 | theme: IThemeService; 21 | tooltipService: ITooltipService; 22 | }; 23 | 24 | interface IPictureInPictureControl { 25 | getElement(): HTMLElement; 26 | 27 | show(): void; 28 | hide(): void; 29 | 30 | destroy(): void; 31 | } 32 | 33 | export { 34 | IPictureInPictureControl, 35 | IPictureInPictureViewStyles, 36 | IPictureInPictureViewCallbacks, 37 | IPictureInPictureViewConfig, 38 | }; 39 | -------------------------------------------------------------------------------- /src/modules/ui/controls/play/play.scss: -------------------------------------------------------------------------------- 1 | @import '../../shared'; 2 | 3 | .playControl { 4 | position: relative; 5 | 6 | display: flex; 7 | 8 | box-sizing: border-box; 9 | 10 | align-items: center; 11 | justify-content: flex-start; 12 | } 13 | 14 | .playbackToggle { 15 | width: 26px; 16 | min-width: 26px; 17 | height: 26px; 18 | min-height: 26px; 19 | 20 | @include query(in-full-screen()) { 21 | width: 35px; 22 | min-width: 35px; 23 | height: 35px; 24 | min-height: 35px; 25 | 26 | .icon_small { 27 | display: none; 28 | } 29 | 30 | .icon_big { 31 | display: block; 32 | } 33 | } 34 | 35 | .playIcon { 36 | display: none; 37 | } 38 | 39 | .pauseIcon { 40 | display: block; 41 | } 42 | 43 | &.paused { 44 | .playIcon { 45 | display: block; 46 | } 47 | 48 | .pauseIcon { 49 | display: none; 50 | } 51 | } 52 | 53 | .icon_small { 54 | display: block; 55 | } 56 | 57 | .icon_big { 58 | display: none; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/modules/ui/controls/play/play.spec.ts: -------------------------------------------------------------------------------- 1 | import 'jsdom-global/register'; 2 | import { expect } from 'chai'; 3 | import * as sinon from 'sinon'; 4 | 5 | import createPlayerTestkit from '../../../../testkit'; 6 | 7 | import { VideoEvent } from '../../../../constants'; 8 | 9 | describe('PlayControl', () => { 10 | let testkit; 11 | let control: any; 12 | let eventEmitter: any; 13 | 14 | beforeEach(() => { 15 | testkit = createPlayerTestkit(); 16 | eventEmitter = testkit.getModule('eventEmitter'); 17 | control = testkit.getModule('playControl'); 18 | }); 19 | 20 | describe('constructor', () => { 21 | it('should create instance ', () => { 22 | expect(control).to.exist; 23 | expect(control.view).to.exist; 24 | }); 25 | }); 26 | 27 | describe('API', () => { 28 | it('should have method for destroying', () => { 29 | const spy = sinon.spy(control, '_unbindEvents'); 30 | expect(control.destroy).to.exist; 31 | control.destroy(); 32 | expect(spy.called).to.be.true; 33 | }); 34 | }); 35 | 36 | describe('video events listeners', () => { 37 | it('should call callback on playback state change', async function() { 38 | const spy = sinon.spy(control, '_updatePlayingState'); 39 | control._bindEvents(); 40 | await eventEmitter.emitAsync(VideoEvent.STATE_CHANGED, {}); 41 | expect(spy.called).to.be.true; 42 | }); 43 | }); 44 | 45 | describe('internal methods', () => { 46 | it('should change playback state', () => { 47 | const playSpy = sinon.stub(control._engine, 'play'); 48 | const pauseSpy = sinon.stub(control._engine, 'pause'); 49 | control._playVideo(); 50 | expect(playSpy.called).to.be.true; 51 | control._pauseVideo(); 52 | expect(pauseSpy.called).to.be.true; 53 | control._engine.play.restore(); 54 | control._engine.pause.restore(); 55 | }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /src/modules/ui/controls/play/play.theme.ts: -------------------------------------------------------------------------------- 1 | import { IThemeConfig } from '../../core/theme/types'; 2 | 3 | export default { 4 | playSvgFill: { 5 | fill: (data: IThemeConfig) => data.color, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /src/modules/ui/controls/play/templates/control.dot: -------------------------------------------------------------------------------- 1 |
3 | 25 |
26 | -------------------------------------------------------------------------------- /src/modules/ui/controls/play/templates/index.ts: -------------------------------------------------------------------------------- 1 | import template from './control.dot'; 2 | const controlTemplate = template.default ? template.default : template; 3 | export { controlTemplate }; 4 | -------------------------------------------------------------------------------- /src/modules/ui/controls/play/types.ts: -------------------------------------------------------------------------------- 1 | import { IThemeService } from '../../core/theme'; 2 | import { ITextMap } from '../../../text-map/types'; 3 | 4 | type IPlayViewStyles = { 5 | playControl: string; 6 | playbackToggle: string; 7 | icon: string; 8 | paused: string; 9 | hidden: string; 10 | }; 11 | 12 | type IPlayViewCallbacks = { 13 | onButtonClick: () => void; 14 | }; 15 | 16 | type IPlayViewConfig = { 17 | callbacks: IPlayViewCallbacks; 18 | textMap: ITextMap; 19 | theme: IThemeService; 20 | }; 21 | 22 | interface IPlayControl { 23 | getElement(): HTMLElement; 24 | 25 | destroy(): void; 26 | } 27 | 28 | export { IPlayControl, IPlayViewStyles, IPlayViewCallbacks, IPlayViewConfig }; 29 | -------------------------------------------------------------------------------- /src/modules/ui/controls/progress/progress.theme.ts: -------------------------------------------------------------------------------- 1 | import { IThemeConfig } from '../../core/theme/types'; 2 | 3 | import { transperentizeColor } from '../../core/theme'; 4 | 5 | export default { 6 | progressPlayed: { 7 | backgroundColor: (data: IThemeConfig) => data.progressColor, 8 | }, 9 | progressSeekTo: { 10 | backgroundColor: (data: IThemeConfig) => 11 | transperentizeColor(data.progressColor, 0.5), 12 | }, 13 | progressSeekBtn: { 14 | backgroundColor: (data: IThemeConfig) => data.progressColor, 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /src/modules/ui/controls/progress/templates/index.ts: -------------------------------------------------------------------------------- 1 | import template1 from './progress.dot'; 2 | import template2 from './progressTimeIndicator.dot'; 3 | 4 | const progressTemplate = template1.default ? template1.default : template1; 5 | const progressTimeIndicatorTemplate = template2.default 6 | ? template2.default 7 | : template2; 8 | 9 | export { progressTemplate, progressTimeIndicatorTemplate }; 10 | -------------------------------------------------------------------------------- /src/modules/ui/controls/progress/templates/progress.dot: -------------------------------------------------------------------------------- 1 |
5 |
6 |
8 |
9 |
12 |
13 |
16 |
17 |
20 |
21 |
24 |
25 |
26 |
29 |
30 |
33 |
35 |
36 |
37 |
40 |
41 |
42 | -------------------------------------------------------------------------------- /src/modules/ui/controls/progress/templates/progressTimeIndicator.dot: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /src/modules/ui/controls/progress/utils/getProgressTimeTooltipPosition.ts: -------------------------------------------------------------------------------- 1 | import { getTooltipPositionByReferenceElement } from '../../../core/tooltip'; 2 | 3 | import { ITooltipPosition } from '../../../core/tooltip/types'; 4 | 5 | function calcProgressTimeTooltipCenterX( 6 | progressPercent: number, 7 | progressElementOffsetX: number, 8 | progressElementWidth: number, 9 | ) { 10 | return ( 11 | progressElementOffsetX + (progressPercent * progressElementWidth) / 100 12 | ); 13 | } 14 | 15 | function getProgressTimeTooltipPosition( 16 | progressPercent: number, 17 | progressElement: HTMLElement, 18 | tooltipContainer: HTMLElement, 19 | ): ITooltipPosition { 20 | return getTooltipPositionByReferenceElement( 21 | progressElement, 22 | tooltipContainer, 23 | (progressElementOffsetX, progressElementWidth) => 24 | calcProgressTimeTooltipCenterX( 25 | progressPercent, 26 | progressElementOffsetX, 27 | progressElementWidth, 28 | ), 29 | ); 30 | } 31 | 32 | export default getProgressTimeTooltipPosition; 33 | -------------------------------------------------------------------------------- /src/modules/ui/controls/time/templates/index.ts: -------------------------------------------------------------------------------- 1 | import template from './time.dot'; 2 | const timeTemplate = template.default ? template.default : template; 3 | export { timeTemplate }; 4 | -------------------------------------------------------------------------------- /src/modules/ui/controls/time/templates/time.dot: -------------------------------------------------------------------------------- 1 |
4 | 7 | 8 | 11 | 12 |
13 | -------------------------------------------------------------------------------- /src/modules/ui/controls/time/time.scss: -------------------------------------------------------------------------------- 1 | @import '../../shared'; 2 | 3 | .timeWrapper { 4 | display: flex; 5 | flex: 0 0 auto; 6 | 7 | height: 25px; 8 | 9 | align-items: center; 10 | } 11 | 12 | .time { 13 | font-size: 12px; 14 | line-height: 12px; 15 | 16 | @include query(in-full-screen()) { 17 | font-size: 14px; 18 | line-height: 14px; 19 | } 20 | } 21 | 22 | .duration { 23 | margin-left: 5px; 24 | 25 | &:before { 26 | margin-right: 4px; 27 | 28 | content: '/'; 29 | } 30 | } 31 | 32 | .liveMode { 33 | .separator { 34 | display: none; 35 | } 36 | 37 | .duration { 38 | display: none; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/modules/ui/controls/time/time.theme.ts: -------------------------------------------------------------------------------- 1 | import { IThemeConfig } from '../../core/theme/types'; 2 | 3 | export default { 4 | timeText: { 5 | color: (data: IThemeConfig) => data.color, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /src/modules/ui/controls/time/types.ts: -------------------------------------------------------------------------------- 1 | import { IThemeService } from '../../core/theme'; 2 | 3 | type ITimeViewStyles = { 4 | timeWrapper: string; 5 | time: string; 6 | current: string; 7 | separator: string; 8 | duration: string; 9 | hidden: string; 10 | }; 11 | 12 | type ITimeViewConfig = { 13 | theme: IThemeService; 14 | }; 15 | 16 | interface ITimeControl { 17 | getElement(): HTMLElement; 18 | 19 | reset(): void; 20 | 21 | show(): void; 22 | hide(): void; 23 | 24 | destroy(): void; 25 | } 26 | 27 | export { ITimeControl, ITimeViewStyles, ITimeViewConfig }; 28 | -------------------------------------------------------------------------------- /src/modules/ui/controls/volume/templates/index.ts: -------------------------------------------------------------------------------- 1 | import template from './control.dot'; 2 | const controlTemplate = template.default ? template.default : template; 3 | export { controlTemplate }; 4 | -------------------------------------------------------------------------------- /src/modules/ui/controls/volume/types.ts: -------------------------------------------------------------------------------- 1 | import { IThemeService } from '../../core/theme'; 2 | import { ITooltipService } from '../../core/tooltip/types'; 3 | import { ITextMap } from '../../../text-map/types'; 4 | 5 | type IVolumeViewStyles = { 6 | volumeControl: string; 7 | volumeInputBlock: string; 8 | isDragging: string; 9 | muteToggle: string; 10 | icon: string; 11 | volume0Icon: string; 12 | volume50Icon: string; 13 | volume100Icon: string; 14 | volume0: string; 15 | volume50: string; 16 | volume100: string; 17 | muted: string; 18 | muteIcon: string; 19 | progressBar: string; 20 | volume: string; 21 | background: string; 22 | hitbox: string; 23 | hidden: string; 24 | }; 25 | 26 | type IVolumeViewCallbacks = { 27 | onVolumeLevelChangeFromInput: (level: number) => void; 28 | onVolumeLevelChangeFromWheel: (delta: number) => void; 29 | onToggleMuteClick: () => void; 30 | onDragStart: () => void; 31 | onDragEnd: () => void; 32 | }; 33 | 34 | type IVolumeViewConfig = { 35 | callbacks: IVolumeViewCallbacks; 36 | textMap: ITextMap; 37 | theme: IThemeService; 38 | tooltipService: ITooltipService; 39 | }; 40 | 41 | interface IVolumeControl { 42 | getElement(): HTMLElement; 43 | 44 | show(): void; 45 | hide(): void; 46 | 47 | destroy(): void; 48 | } 49 | 50 | export { 51 | IVolumeControl, 52 | IVolumeViewStyles, 53 | IVolumeViewCallbacks, 54 | IVolumeViewConfig, 55 | }; 56 | -------------------------------------------------------------------------------- /src/modules/ui/controls/volume/volume.theme.ts: -------------------------------------------------------------------------------- 1 | import { IThemeConfig } from '../../core/theme/types'; 2 | 3 | import { transperentizeColor } from '../../core/theme'; 4 | 5 | export default { 6 | volumeSvgFill: { 7 | fill: (data: IThemeConfig) => data.color, 8 | }, 9 | volumeSvgStroke: { 10 | stroke: (data: IThemeConfig) => data.color, 11 | }, 12 | volumeProgress: { 13 | backgroundColor: (data: IThemeConfig) => data.color, 14 | '&:after': { 15 | backgroundColor: (data: IThemeConfig) => data.color, 16 | }, 17 | }, 18 | volumeProgressBackground: { 19 | backgroundColor: (data: IThemeConfig) => 20 | transperentizeColor(data.color, 0.25), 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /src/modules/ui/core/element-queries/element-queries.ts: -------------------------------------------------------------------------------- 1 | import getQueriesForElement from './getQueriesForElement'; 2 | 3 | const DEFAULT_QUERY_PREFIX = 'data-playable'; 4 | 5 | class ElementQueries { 6 | private _element: HTMLElement; 7 | private _queryPrefix: string; 8 | private _queries: { mode: string; width: number }[]; 9 | 10 | constructor(element: HTMLElement, { prefix = DEFAULT_QUERY_PREFIX } = {}) { 11 | this._element = element; 12 | this._queryPrefix = prefix; 13 | this._queries = []; 14 | } 15 | 16 | private _getQueryAttributeValue(mode: string, elementWidth: number) { 17 | return this._queries 18 | .filter( 19 | query => 20 | query.mode === mode && 21 | ((mode === 'max' && query.width >= elementWidth) || 22 | (mode === 'min' && query.width <= elementWidth)), 23 | ) 24 | .map(query => `${query.width}px`) 25 | .join(' '); 26 | } 27 | 28 | private _setQueryAttribute(mode: string, elementWidth: number) { 29 | const attributeName = this._queryPrefix 30 | ? `${this._queryPrefix}-${mode}-width` 31 | : `${mode}-width`; 32 | const attributeValue = this._getQueryAttributeValue(mode, elementWidth); 33 | 34 | if (attributeValue) { 35 | this._element.setAttribute(attributeName, attributeValue); 36 | } else { 37 | this._element.removeAttribute(attributeName); 38 | } 39 | } 40 | 41 | getQueries() { 42 | this._queries = getQueriesForElement(this._element, this._queryPrefix); 43 | } 44 | 45 | setWidth(width: number) { 46 | this._setQueryAttribute('min', width); 47 | this._setQueryAttribute('max', width); 48 | } 49 | 50 | destroy() { 51 | this._element = null; 52 | } 53 | } 54 | 55 | export default ElementQueries; 56 | -------------------------------------------------------------------------------- /src/modules/ui/core/element-queries/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './element-queries'; 2 | -------------------------------------------------------------------------------- /src/modules/ui/core/element-queries/isElementMatchesSelector.ts: -------------------------------------------------------------------------------- 1 | const ALIASES = [ 2 | 'matches', 3 | 'webkitMatchesSelector', 4 | 'mozMatchesSelector', 5 | 'msMatchesSelector', 6 | ]; 7 | 8 | let matchesSelectorFn: Function; 9 | 10 | if (typeof HTMLElement !== 'undefined') { 11 | for (let i = 0; i < ALIASES.length; i++) { 12 | matchesSelectorFn = (Element as any).prototype[ALIASES[i]] as Function; 13 | 14 | if (matchesSelectorFn) { 15 | break; 16 | } 17 | } 18 | } 19 | 20 | const isElementMatchesSelector = matchesSelectorFn 21 | ? (element: HTMLElement, selector: string) => 22 | matchesSelectorFn.call(element, selector) 23 | : (element: HTMLElement, selector: string) => 24 | Array.prototype.indexOf.call( 25 | document.querySelectorAll(selector), 26 | element, 27 | ) !== -1; 28 | 29 | export default isElementMatchesSelector; 30 | -------------------------------------------------------------------------------- /src/modules/ui/core/element-queries/utils.ts: -------------------------------------------------------------------------------- 1 | function reduce( 2 | arrayLike: { length: number }, 3 | callback: (...args: any[]) => void, 4 | initialValue: any, 5 | ) { 6 | return Array.prototype.reduce.call(arrayLike, callback, initialValue); 7 | } 8 | 9 | function forEachMatch( 10 | string: string, 11 | pattern: RegExp, 12 | callback: (match: RegExpExecArray) => void, 13 | ) { 14 | let match = pattern.exec(string); 15 | 16 | while (match !== null) { 17 | callback(match); 18 | match = pattern.exec(string); 19 | } 20 | } 21 | 22 | export { reduce, forEachMatch }; 23 | -------------------------------------------------------------------------------- /src/modules/ui/core/extendStyles.ts: -------------------------------------------------------------------------------- 1 | import classnames from 'classnames'; 2 | import { IStyles } from './types'; 3 | 4 | function extendStyles(sourceStyles: IStyles, partialStyles: IStyles) { 5 | const styles = { 6 | ...sourceStyles, 7 | }; 8 | 9 | Object.keys(partialStyles).forEach(styleName => { 10 | styles[styleName] = styles[styleName] 11 | ? classnames(styles[styleName], partialStyles[styleName]) 12 | : partialStyles[styleName]; 13 | }); 14 | 15 | return styles; 16 | } 17 | 18 | export default extendStyles; 19 | -------------------------------------------------------------------------------- /src/modules/ui/core/getElementByHook.ts: -------------------------------------------------------------------------------- 1 | function getElementByHook(element: HTMLElement, hook: string): HTMLElement { 2 | return element.querySelector(`[data-playable-hook="${hook}"]`); 3 | } 4 | 5 | export default getElementByHook; 6 | -------------------------------------------------------------------------------- /src/modules/ui/core/htmlToElement.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import htmlToElement from './htmlToElement'; 4 | 5 | describe('htmlToElement', () => { 6 | it('should create dom element from given string with HTML', () => { 7 | const html = '
TEST
'; 8 | const element = htmlToElement(html); 9 | 10 | expect(element.constructor).to.be.equal(HTMLDivElement); 11 | }); 12 | 13 | it('should throw error if provided HTML is empty', () => { 14 | const html = ''; 15 | 16 | expect(() => htmlToElement(html)).to.throw(); 17 | }); 18 | 19 | it("should throw error if provided HTML doesn't have root element", () => { 20 | const html = 'asdasd'; 21 | 22 | expect(() => htmlToElement(html)).to.throw(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/modules/ui/core/htmlToElement.ts: -------------------------------------------------------------------------------- 1 | function htmlToElement(html: string) { 2 | if (!html) { 3 | throw new Error('HTML provided to htmlToElement is empty'); 4 | } 5 | 6 | const div: HTMLDivElement = document.createElement('div'); 7 | div.innerHTML = html.trim(); 8 | 9 | if (div.childElementCount > 1) { 10 | throw new Error("HTML provided to htmlToElement doesn't have root element"); 11 | } 12 | 13 | return div.firstChild as HTMLElement; 14 | } 15 | 16 | export default htmlToElement; 17 | -------------------------------------------------------------------------------- /src/modules/ui/core/stylable.spec.ts: -------------------------------------------------------------------------------- 1 | import Chance from 'chance'; 2 | import { expect } from 'chai'; 3 | 4 | import Stylable from './stylable'; 5 | 6 | const chance = new Chance(); 7 | 8 | describe('Stylable', () => { 9 | let stylable: any; 10 | 11 | beforeEach(() => { 12 | stylable = new Stylable(); 13 | }); 14 | 15 | afterEach(() => { 16 | Stylable.resetStyles(); 17 | }); 18 | 19 | it('instance should have method for getting styles', () => { 20 | expect(stylable.styleNames).to.be.deep.equal({}); 21 | }); 22 | 23 | it('should have method for extending styles', () => { 24 | const styleNames = { 25 | name: 'value', 26 | }; 27 | Stylable.extendStyleNames(styleNames); 28 | expect(stylable.styleNames).to.be.deep.equal(styleNames); 29 | }); 30 | 31 | it('method for extending styles should merge styleNames for same style', () => { 32 | const styleNames1 = { 33 | name: chance.word(), 34 | }; 35 | 36 | const styleNames2 = { 37 | name: chance.word(), 38 | }; 39 | 40 | Stylable.extendStyleNames(styleNames2); 41 | Stylable.extendStyleNames(styleNames1); 42 | 43 | expect(stylable.styleNames.name.split(' ')).to.include.members([ 44 | styleNames1.name, 45 | styleNames2.name, 46 | ]); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/modules/ui/core/stylable.ts: -------------------------------------------------------------------------------- 1 | import extendStyles from './extendStyles'; 2 | import { IThemeService, ICSSRules } from './theme'; 3 | import { IStyles, IStylable } from './types'; 4 | 5 | class Stylable implements IStylable { 6 | private static _moduleTheme: ICSSRules; 7 | private static _styles: IStyles = {}; 8 | 9 | private _themeStyles: IStyles = {}; 10 | 11 | constructor(theme?: IThemeService) { 12 | const moduleTheme = (this.constructor as typeof Stylable)._moduleTheme; 13 | if (theme && moduleTheme) { 14 | theme.registerModuleTheme(this, moduleTheme); 15 | this._themeStyles = theme.get(this); 16 | } 17 | } 18 | 19 | static setTheme(theme: ICSSRules) { 20 | this._moduleTheme = theme; 21 | } 22 | 23 | static extendStyleNames(styles: IStyles) { 24 | this._styles = extendStyles(this._styles, styles); 25 | } 26 | 27 | static resetStyles() { 28 | this._styles = {}; 29 | } 30 | 31 | get themeStyles(): IStyles { 32 | return this._themeStyles; 33 | } 34 | 35 | get styleNames(): TStyles { 36 | // NOTE: TS does not work with instance static fields + generic type 37 | return (this.constructor as any)._styles || {}; 38 | } 39 | } 40 | 41 | export default Stylable; 42 | -------------------------------------------------------------------------------- /src/modules/ui/core/theme/index.ts: -------------------------------------------------------------------------------- 1 | import { IThemeConfig, ICSSRules } from './types'; 2 | import transperentizeColor from './utils/transperentizeColor'; 3 | import ThemeService, { 4 | IThemeAPI, 5 | IThemeService, 6 | DEFAULT_THEME_CONFIG, 7 | } from './theme-service'; 8 | 9 | export { 10 | IThemeAPI, 11 | transperentizeColor, 12 | DEFAULT_THEME_CONFIG, 13 | IThemeConfig, 14 | IThemeService, 15 | ICSSRules, 16 | }; 17 | 18 | export default ThemeService; 19 | -------------------------------------------------------------------------------- /src/modules/ui/core/theme/theme-service.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from './style-sheet'; 2 | 3 | import playerAPI from '../../../../core/player-api-decorator'; 4 | 5 | import { IThemeAPI, IThemeService, ICSSRules, IThemeConfig } from './types'; 6 | 7 | const DEFAULT_THEME_CONFIG = { 8 | color: '#FFF', 9 | liveColor: '#ea492e', 10 | progressColor: '#FFF', 11 | }; 12 | 13 | class ThemeService implements IThemeService { 14 | static moduleName = 'theme'; 15 | static dependencies = ['themeConfig']; 16 | 17 | private _styleSheet: StyleSheet; 18 | 19 | constructor({ themeConfig }: { themeConfig: IThemeConfig }) { 20 | this._styleSheet = new StyleSheet(); 21 | 22 | this._styleSheet.update({ 23 | ...DEFAULT_THEME_CONFIG, 24 | ...themeConfig, 25 | }); 26 | 27 | // setTimeout here is for calling `attach` after all modules resolved. 28 | window.setTimeout(() => { 29 | this._styleSheet && this._styleSheet.attach(); 30 | }, 0); 31 | } 32 | 33 | /** 34 | * Method for setting theme for player instance 35 | * 36 | * @example 37 | * player.updateTheme({ 38 | * progressColor: "#AEAD22" 39 | * }) 40 | * @note 41 | * 42 | * You can check info about theming [here](/themes) 43 | * 44 | * @param themeConfig - Theme config 45 | * 46 | */ 47 | @playerAPI() 48 | updateTheme(themeConfig: IThemeConfig) { 49 | this._styleSheet.update(themeConfig); 50 | } 51 | 52 | registerModuleTheme(module: object, rules: ICSSRules) { 53 | this._styleSheet.registerModuleTheme(module, rules); 54 | } 55 | 56 | get(module: any) { 57 | return this._styleSheet.getModuleClassNames(module); 58 | } 59 | 60 | destroy() { 61 | this._styleSheet = null; 62 | } 63 | } 64 | 65 | export { DEFAULT_THEME_CONFIG, IThemeService, IThemeAPI }; 66 | 67 | export default ThemeService; 68 | -------------------------------------------------------------------------------- /src/modules/ui/core/theme/types.ts: -------------------------------------------------------------------------------- 1 | import { IStyles } from '../types'; 2 | 3 | type ICSSRuleFunction = (data: any) => string; 4 | 5 | type ICSSRule = { 6 | [cssPropName: string]: string | ICSSRuleFunction | ICSSRule; 7 | }; 8 | 9 | type ICSSRules = { 10 | [classImportName: string]: ICSSRule; 11 | }; 12 | 13 | type IThemeConfig = { 14 | color?: string; 15 | progressColor?: string; 16 | }; 17 | 18 | interface IThemeService { 19 | updateTheme(config: IThemeConfig): void; 20 | registerModuleTheme(module: object, rules: ICSSRules): void; 21 | get(module: object): IStyles; 22 | destroy(): void; 23 | } 24 | 25 | interface IThemeAPI { 26 | updateTheme?: (themeConfig: IThemeConfig) => void; 27 | } 28 | 29 | export { 30 | IThemeAPI, 31 | IThemeService, 32 | ICSSRuleFunction, 33 | ICSSRule, 34 | ICSSRules, 35 | IThemeConfig, 36 | }; 37 | -------------------------------------------------------------------------------- /src/modules/ui/core/theme/utils/camelToKebab.ts: -------------------------------------------------------------------------------- 1 | function camelToKebab(string: string): string { 2 | return string.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); 3 | } 4 | 5 | export default camelToKebab; 6 | -------------------------------------------------------------------------------- /src/modules/ui/core/theme/utils/generateClassNames.ts: -------------------------------------------------------------------------------- 1 | import getUniqueId from './getUniqueId'; 2 | 3 | import { IStyles } from '../../types'; 4 | import { ICSSRules } from '../types'; 5 | 6 | function getUniqueClassName(classImportName: string) { 7 | return `wix-playable--${getUniqueId(classImportName)}`; 8 | } 9 | 10 | function generateClassNames(rules: ICSSRules): IStyles { 11 | return Object.keys(rules).reduce( 12 | (acc, classImportName) => ({ 13 | ...acc, 14 | [classImportName]: getUniqueClassName(classImportName), 15 | }), 16 | {}, 17 | ); 18 | } 19 | 20 | export default generateClassNames; 21 | -------------------------------------------------------------------------------- /src/modules/ui/core/theme/utils/getUniqueId.ts: -------------------------------------------------------------------------------- 1 | import camelToKebab from './camelToKebab'; 2 | 3 | const generatedIds: Map = new Map(); 4 | 5 | function getUniquePostfix(className: string): string { 6 | if (generatedIds.has(className)) { 7 | const newID: number = generatedIds.get(className) + 1; 8 | generatedIds.set(className, newID); 9 | 10 | return `${newID}`; 11 | } 12 | 13 | generatedIds.set(className, 0); 14 | return ''; 15 | } 16 | 17 | function getUniqueId(classImportName: string): string { 18 | const kebabName: string = camelToKebab(classImportName); 19 | 20 | return `${kebabName}${getUniquePostfix(kebabName)}`; 21 | } 22 | 23 | export default getUniqueId; 24 | -------------------------------------------------------------------------------- /src/modules/ui/core/theme/utils/hexToRgb.ts: -------------------------------------------------------------------------------- 1 | const SHORTHAND_HEX_COLOR_PATTERN = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; 2 | const HEX_COLOR_PATTERN = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i; 3 | 4 | type IRGB = { 5 | r: number; 6 | g: number; 7 | b: number; 8 | }; 9 | 10 | function hexToRgb(hex: string): IRGB { 11 | hex = hex.replace( 12 | SHORTHAND_HEX_COLOR_PATTERN, 13 | (_, r, g, b) => r + r + g + g + b + b, 14 | ); 15 | 16 | const result = hex.match(HEX_COLOR_PATTERN); 17 | if (result) { 18 | return { 19 | r: parseInt(result[1], 16), 20 | g: parseInt(result[2], 16), 21 | b: parseInt(result[3], 16), 22 | }; 23 | } 24 | 25 | throw new Error('Playable.js: Color passed to theme should be in HEX format'); 26 | } 27 | 28 | export default hexToRgb; 29 | -------------------------------------------------------------------------------- /src/modules/ui/core/theme/utils/transperentizeColor.ts: -------------------------------------------------------------------------------- 1 | import hexToRgb from './hexToRgb'; 2 | 3 | function transperentizeColor(color: string, alpha: number = 1) { 4 | const { r, g, b } = hexToRgb(color); 5 | return `rgba(${r},${g},${b},${alpha})`; 6 | } 7 | 8 | export default transperentizeColor; 9 | -------------------------------------------------------------------------------- /src/modules/ui/core/toggleElementClass.ts: -------------------------------------------------------------------------------- 1 | function toggleElementClass( 2 | element: HTMLElement, 3 | className: string, 4 | shouldAdd: boolean, 5 | ) { 6 | if (shouldAdd) { 7 | element.classList.add(className); 8 | } else { 9 | element.classList.remove(className); 10 | } 11 | } 12 | 13 | export default toggleElementClass; 14 | -------------------------------------------------------------------------------- /src/modules/ui/core/tooltip/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Tooltip } from './tooltip'; 2 | export { default as TooltipService } from './tooltip-service'; 3 | export { default as getTooltipPositionByReferenceElement } from './utils/getTooltipPositionByReferenceElement'; 4 | -------------------------------------------------------------------------------- /src/modules/ui/core/tooltip/templates/index.ts: -------------------------------------------------------------------------------- 1 | import template1 from './tooltip.dot'; 2 | import template2 from './tooltipContainer.dot'; 3 | 4 | const tooltipTemplate = template1.default ? template1.default : template1; 5 | const tooltipContainerTemplate = template2.default 6 | ? template2.default 7 | : template2; 8 | 9 | export { tooltipTemplate, tooltipContainerTemplate }; 10 | -------------------------------------------------------------------------------- /src/modules/ui/core/tooltip/templates/tooltip.dot: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/modules/ui/core/tooltip/templates/tooltipContainer.dot: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /src/modules/ui/core/tooltip/tooltip-container.scss: -------------------------------------------------------------------------------- 1 | $tooltipMargin: 10px; 2 | $progressBarOffset: 4px; 3 | 4 | .tooltipContainer { 5 | position: absolute; 6 | top: 0; 7 | right: 0; 8 | bottom: 0; 9 | left: 0; 10 | 11 | display: flex; 12 | 13 | margin: $tooltipMargin $tooltipMargin $tooltipMargin - $progressBarOffset; 14 | } 15 | -------------------------------------------------------------------------------- /src/modules/ui/core/tooltip/tooltip.scss: -------------------------------------------------------------------------------- 1 | @import '../../shared'; 2 | 3 | .tooltip { 4 | position: absolute; 5 | z-index: 100; 6 | 7 | visibility: hidden; 8 | 9 | transition: opacity .2s, visibility .2s; 10 | 11 | opacity: 0; 12 | 13 | &.showAsText { 14 | padding: 4px 5px; 15 | 16 | background: rgba(0, 0, 0, .5); 17 | } 18 | 19 | 20 | 21 | &.tooltipVisible { 22 | visibility: visible; 23 | 24 | opacity: 1; 25 | } 26 | } 27 | .showAsText { 28 | .tooltipInner { 29 | font-size: 11px; 30 | line-height: 12px; 31 | 32 | white-space: nowrap; 33 | 34 | color: white; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/modules/ui/core/tooltip/types.ts: -------------------------------------------------------------------------------- 1 | enum TooltipPositionPlacement { 2 | TOP = 'top', 3 | BOTTOM = 'bottom', 4 | } 5 | 6 | type ITooltipPosition = { 7 | placement: TooltipPositionPlacement; 8 | x: number; 9 | }; 10 | 11 | type ITooltipPositionFunction = ( 12 | tooltipContainerElement: HTMLElement, 13 | ) => ITooltipPosition; 14 | 15 | type ITooltipShowOptions = { 16 | text?: string; 17 | element?: HTMLElement; 18 | position: ITooltipPosition | ITooltipPositionFunction; 19 | }; 20 | 21 | type ITooltipStyles = { 22 | tooltip: string; 23 | tooltipVisible: string; 24 | tooltipInner: string; 25 | showAsText: string; 26 | showAsElement: string; 27 | }; 28 | 29 | interface ITooltip { 30 | getElement(): HTMLElement; 31 | isHidden: boolean; 32 | show(): void; 33 | hide(): void; 34 | setText(text: string): void; 35 | setStyle(style: { [key: string]: string | number }): void; 36 | destroy(): void; 37 | } 38 | 39 | type ITooltipReferenceOptions = { 40 | text?: string; 41 | element?: HTMLElement; 42 | }; 43 | 44 | interface ITooltipReference { 45 | isHidden: boolean; 46 | isDisabled: boolean; 47 | show(): void; 48 | hide(): void; 49 | setText(text: string): void; 50 | disable(): void; 51 | enable(): void; 52 | destroy(): void; 53 | } 54 | 55 | interface ITooltipService { 56 | isHidden: boolean; 57 | tooltipContainerElement: HTMLElement; 58 | setText(text: string): void; 59 | show(options: ITooltipShowOptions): void; 60 | hide(): void; 61 | createReference( 62 | reference: HTMLElement, 63 | options: ITooltipReferenceOptions, 64 | ): ITooltipReference; 65 | destroy(): void; 66 | } 67 | 68 | export { 69 | TooltipPositionPlacement, 70 | ITooltipPosition, 71 | ITooltipPositionFunction, 72 | ITooltipShowOptions, 73 | ITooltipStyles, 74 | ITooltip, 75 | ITooltipReference, 76 | ITooltipReferenceOptions, 77 | ITooltipService, 78 | }; 79 | -------------------------------------------------------------------------------- /src/modules/ui/core/tooltip/utils/getTooltipPositionByReferenceElement.ts: -------------------------------------------------------------------------------- 1 | import { ITooltipPosition, TooltipPositionPlacement } from '../types'; 2 | 3 | type ITooltipCenterXfn = ( 4 | tooltipReferenceOffsetX: number, 5 | tooltipReferenceWidth: number, 6 | ) => number; 7 | 8 | function calcTooltipCenterX( 9 | tooltipReferenceOffsetX: number, 10 | tooltipReferenceWidth: number, 11 | ) { 12 | return tooltipReferenceOffsetX + tooltipReferenceWidth / 2; 13 | } 14 | 15 | function getTooltipPositionByReferenceElement( 16 | tooltipReferenceElement: HTMLElement, 17 | tooltipContainerElement: HTMLElement, 18 | tooltipCenterXfn: ITooltipCenterXfn = calcTooltipCenterX, 19 | ): ITooltipPosition { 20 | const tooltipReferenceRect = tooltipReferenceElement.getBoundingClientRect(); 21 | const tooltipContainerRect = tooltipContainerElement.getBoundingClientRect(); 22 | 23 | const tooltipPlacement = 24 | tooltipReferenceRect.top > tooltipContainerRect.top 25 | ? TooltipPositionPlacement.BOTTOM 26 | : TooltipPositionPlacement.TOP; 27 | const tooltipReferenceOffsetX = 28 | tooltipReferenceRect.left - tooltipContainerRect.left; 29 | const tooltipCenterX = tooltipCenterXfn( 30 | tooltipReferenceOffsetX, 31 | tooltipReferenceRect.width, 32 | ); 33 | 34 | return { placement: tooltipPlacement, x: tooltipCenterX }; 35 | } 36 | 37 | export default getTooltipPositionByReferenceElement; 38 | -------------------------------------------------------------------------------- /src/modules/ui/core/types.ts: -------------------------------------------------------------------------------- 1 | type IStyles = { 2 | [className: string]: string; 3 | }; 4 | 5 | interface IStylable { 6 | styleNames: TStyles; 7 | } 8 | 9 | interface IView extends IStylable {} 10 | 11 | export { IStyles, IStylable, IView }; 12 | -------------------------------------------------------------------------------- /src/modules/ui/core/utils/formatTime.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import formatTime from './formatTime'; 4 | 5 | describe('formatTime', () => { 6 | it('should return valid string', () => { 7 | expect(formatTime(NaN)).to.be.equal('00:00'); 8 | expect(formatTime(Infinity)).to.be.equal('00:00'); 9 | expect(formatTime(0)).to.be.equal('00:00'); 10 | expect(formatTime(10)).to.be.equal('00:10'); 11 | expect(formatTime(110)).to.be.equal('01:50'); 12 | expect(formatTime(11100)).to.be.equal('03:05:00'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/modules/ui/core/utils/formatTime.ts: -------------------------------------------------------------------------------- 1 | function formatTime(seconds: number): string { 2 | const isValid = !isNaN(seconds) && isFinite(seconds); 3 | const isNegative = isValid && seconds < 0; 4 | const date = new Date(null); 5 | 6 | date.setSeconds(isValid ? Math.abs(Math.floor(seconds)) : 0); 7 | 8 | // get HH:mm:ss part, remove hours if they are "00:" 9 | const time = date 10 | .toISOString() 11 | .substr(11, 8) 12 | .replace(/^00:/, ''); 13 | 14 | return isNegative ? `-${time}` : time; 15 | } 16 | 17 | export default formatTime; 18 | -------------------------------------------------------------------------------- /src/modules/ui/core/view.ts: -------------------------------------------------------------------------------- 1 | import Stylable from './stylable'; 2 | import { IView, IStyles } from './types'; 3 | 4 | class View extends Stylable 5 | implements IView {} 6 | 7 | export default View; 8 | -------------------------------------------------------------------------------- /src/modules/ui/debug-panel/debug-panel.scss: -------------------------------------------------------------------------------- 1 | @import '../shared'; 2 | 3 | .debugPanel { 4 | position: absolute; 5 | z-index: 10000; 6 | top: 10px; 7 | left: 10px; 8 | 9 | overflow: scroll; 10 | 11 | width: 400px; 12 | height: 250px; 13 | 14 | border-radius: 3px; 15 | background-color: rgba(0, 0, 0, .95); 16 | 17 | .closeButton { 18 | position: absolute; 19 | top: 10px; 20 | right: 5px; 21 | 22 | cursor: pointer; 23 | 24 | color: white; 25 | 26 | &:hover { 27 | opacity: .8; 28 | } 29 | } 30 | 31 | .infoContainer { 32 | font-size: 8px; 33 | line-height: 8px; 34 | 35 | margin: 5px; 36 | padding: 5px; 37 | 38 | color: white; 39 | 40 | .string { 41 | color: green; 42 | } 43 | .number { 44 | color: darkorange; 45 | } 46 | .boolean { 47 | color: blue; 48 | } 49 | .null { 50 | color: magenta; 51 | } 52 | .key { 53 | color: white; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/modules/ui/debug-panel/syntaxHighlight.ts: -------------------------------------------------------------------------------- 1 | import { IDebugPanelHighlightStyles } from './types'; 2 | 3 | function syntaxHighlight(json: string, styleNames: IDebugPanelHighlightStyles) { 4 | json = json 5 | .replace(/&/g, '&') 6 | .replace(//g, '>'); 8 | 9 | return json.replace( 10 | /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g, 11 | (match: string) => { 12 | let cls = styleNames.number; 13 | if (/^"/.test(match)) { 14 | if (/:$/.test(match)) { 15 | cls = styleNames.key; 16 | } else { 17 | cls = styleNames.string; 18 | } 19 | } else if (/true|false/.test(match)) { 20 | cls = styleNames.boolean; 21 | } else if (/null/.test(match)) { 22 | cls = styleNames.null; 23 | } 24 | return `${match}`; 25 | }, 26 | ); 27 | } 28 | 29 | export default syntaxHighlight; 30 | -------------------------------------------------------------------------------- /src/modules/ui/debug-panel/templates/debug-panel.dot: -------------------------------------------------------------------------------- 1 |
2 |
6 | x 7 |
8 |
12 |   
13 |
14 | -------------------------------------------------------------------------------- /src/modules/ui/debug-panel/templates/index.ts: -------------------------------------------------------------------------------- 1 | import template from './debug-panel.dot'; 2 | const debugPanelTemplate = template.default ? template.default : template; 3 | export { debugPanelTemplate }; 4 | -------------------------------------------------------------------------------- /src/modules/ui/debug-panel/types.ts: -------------------------------------------------------------------------------- 1 | type IDebugPanelHighlightStyles = { 2 | key: string; 3 | number: string; 4 | string: string; 5 | boolean: string; 6 | null: string; 7 | }; 8 | 9 | type IDebugPanelViewStyles = IDebugPanelHighlightStyles & { 10 | debugPanel: string; 11 | infoContainer: string; 12 | closeButton: string; 13 | hidden: string; 14 | }; 15 | 16 | type IDebugPanelViewCallbacks = { 17 | onCloseButtonClick: EventListenerOrEventListenerObject; 18 | }; 19 | 20 | type IDebugPanelViewConfig = { 21 | callbacks: IDebugPanelViewCallbacks; 22 | }; 23 | 24 | export { 25 | IDebugPanelHighlightStyles, 26 | IDebugPanelViewStyles, 27 | IDebugPanelViewCallbacks, 28 | IDebugPanelViewConfig, 29 | }; 30 | -------------------------------------------------------------------------------- /src/modules/ui/interaction-indicator/interaction-indicator.scss: -------------------------------------------------------------------------------- 1 | @import '../shared'; 2 | 3 | .iconContainer { 4 | position: absolute; 5 | z-index: 100; 6 | top: 0; 7 | right: 0; 8 | bottom: 0; 9 | left: 0; 10 | 11 | display: flex; 12 | 13 | pointer-events: none; 14 | 15 | justify-content: center; 16 | align-items: center; 17 | 18 | .icon { 19 | font-size: 9px; 20 | line-height: 9px; 21 | 22 | position: relative; 23 | 24 | display: flex; 25 | 26 | animation-name: fadeOut; 27 | animation-duration: .5s; 28 | 29 | opacity: 0; 30 | border-radius: 100px; 31 | background-color: rgba(0,0,0, .5); 32 | 33 | justify-content: center; 34 | align-items: center; 35 | } 36 | 37 | .animatedIcon { 38 | animation-name: iconSize; 39 | animation-duration: .5s; 40 | } 41 | 42 | .playIcon { 43 | position: relative; 44 | left: 3px; 45 | } 46 | 47 | .pauseIcon { 48 | margin: 5px 0; 49 | } 50 | 51 | .seconds { 52 | position: absolute; 53 | top: 0; 54 | right: 0; 55 | bottom: 0; 56 | left: 0; 57 | 58 | display: flex; 59 | 60 | min-width: 5px; 61 | min-height: 8px; 62 | 63 | color: white; 64 | 65 | justify-content: center; 66 | align-items: center; 67 | 68 | span { 69 | display: block; 70 | } 71 | } 72 | } 73 | 74 | @keyframes iconSize { 75 | from { 76 | width: 22px; 77 | height: 22px; 78 | } 79 | 80 | to { 81 | width: 30px; 82 | height: 30px; 83 | } 84 | } 85 | 86 | @keyframes fadeOut { 87 | from { 88 | width: 22px; 89 | height: 22px; 90 | padding: 19px; 91 | 92 | opacity: .9; 93 | } 94 | 95 | to { 96 | font-size: 14px; 97 | line-height: 14px; 98 | 99 | width: 30px; 100 | height: 30px; 101 | padding: 25px; 102 | 103 | opacity: 0; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/modules/ui/interaction-indicator/templates/container.dot: -------------------------------------------------------------------------------- 1 |
2 |
3 | -------------------------------------------------------------------------------- /src/modules/ui/interaction-indicator/templates/decreaseVolumeIcon.dot: -------------------------------------------------------------------------------- 1 |
2 | 5 | 7 | 9 | 10 | 11 | 12 |
13 | -------------------------------------------------------------------------------- /src/modules/ui/interaction-indicator/templates/forwardIcon.dot: -------------------------------------------------------------------------------- 1 |
2 |
3 | ${props.texts.SECONDS_COUNT} 4 |
5 | 8 | 11 | 12 |
13 | -------------------------------------------------------------------------------- /src/modules/ui/interaction-indicator/templates/increaseVolumeIcon.dot: -------------------------------------------------------------------------------- 1 |
2 | 5 | 7 | 9 | 11 | 12 | 13 |
14 | -------------------------------------------------------------------------------- /src/modules/ui/interaction-indicator/templates/index.ts: -------------------------------------------------------------------------------- 1 | import template1 from './container.dot'; 2 | import template2 from './playIcon.dot'; 3 | import template3 from './pauseIcon.dot'; 4 | import template4 from './forwardIcon.dot'; 5 | import template5 from './rewindIcon.dot'; 6 | import template6 from './increaseVolumeIcon.dot'; 7 | import template7 from './decreaseVolumeIcon.dot'; 8 | import template8 from './muteIcon.dot'; 9 | 10 | const containerTemplate = template1.default ? template1.default : template1; 11 | const playIconTemplate = template2.default ? template2.default : template2; 12 | const pauseIconTemplate = template3.default ? template3.default : template3; 13 | const forwardIconTemplate = template4.default ? template4.default : template4; 14 | const rewindIconTemplate = template5.default ? template5.default : template5; 15 | const increaseVolumeIconTemplate = template6.default 16 | ? template6.default 17 | : template6; 18 | const decreaseVolumeIconTemplate = template7.default 19 | ? template7.default 20 | : template7; 21 | const muteIconTemplate = template8.default ? template8.default : template8; 22 | 23 | export { 24 | containerTemplate, 25 | pauseIconTemplate, 26 | playIconTemplate, 27 | forwardIconTemplate, 28 | rewindIconTemplate, 29 | increaseVolumeIconTemplate, 30 | decreaseVolumeIconTemplate, 31 | muteIconTemplate, 32 | }; 33 | -------------------------------------------------------------------------------- /src/modules/ui/interaction-indicator/templates/muteIcon.dot: -------------------------------------------------------------------------------- 1 |
2 | 5 | 7 | 9 | 11 | 12 | 13 |
14 | -------------------------------------------------------------------------------- /src/modules/ui/interaction-indicator/templates/pauseIcon.dot: -------------------------------------------------------------------------------- 1 |
2 | 5 | 8 | 9 |
10 | -------------------------------------------------------------------------------- /src/modules/ui/interaction-indicator/templates/playIcon.dot: -------------------------------------------------------------------------------- 1 |
2 | 5 | 8 | 9 |
10 | -------------------------------------------------------------------------------- /src/modules/ui/interaction-indicator/templates/rewindIcon.dot: -------------------------------------------------------------------------------- 1 |
2 |
3 | ${props.texts.SECONDS_COUNT} 4 |
5 | 8 | 11 | 12 |
13 | -------------------------------------------------------------------------------- /src/modules/ui/interaction-indicator/types.ts: -------------------------------------------------------------------------------- 1 | type IInteractionIndicatorViewStyles = { 2 | iconContainer: string; 3 | icon: string; 4 | animatedIcon: string; 5 | playIcon: string; 6 | pauseIcon: string; 7 | seconds: string; 8 | hidden: string; 9 | }; 10 | 11 | interface IInteractionIndicator { 12 | getElement(): HTMLElement; 13 | 14 | showPause(): void; 15 | showPlay(): void; 16 | showRewind(): void; 17 | showForward(): void; 18 | showMute(): void; 19 | showIncreaseVolume(): void; 20 | showDecreaseVolume(): void; 21 | hideIcons(): void; 22 | 23 | show(): void; 24 | hide(): void; 25 | 26 | destroy(): void; 27 | } 28 | 29 | export { IInteractionIndicator, IInteractionIndicatorViewStyles }; 30 | -------------------------------------------------------------------------------- /src/modules/ui/live-indicator/live-indicator.scss: -------------------------------------------------------------------------------- 1 | @import '../shared'; 2 | 3 | $active-color: #ea492e; 4 | $inactive-color: #959595; 5 | 6 | .liveIndicator { 7 | position: relative; 8 | // position styles end 9 | 10 | transition: background-color .2s; 11 | 12 | background-color: $inactive-color; 13 | // position styles start 14 | 15 | @include query(ltr()) { 16 | margin-right: 15px; 17 | 18 | direction: ltr; 19 | } 20 | @include query(rtl()) { 21 | margin-left: 15px; 22 | 23 | direction: rtl; 24 | } 25 | @include query(in-full-screen(), ltr()) { 26 | margin-right: 20px; 27 | } 28 | @include query(in-full-screen(), rtl()) { 29 | margin-left: 20px; 30 | } 31 | @include query(max-width-550(), ltr()) { 32 | margin-right: 10px; 33 | } 34 | @include query(max-width-550(), rtl()) { 35 | margin-left: 10px; 36 | } 37 | @include query(max-width-280()) { 38 | padding: 2px 3px; 39 | } 40 | @include query(max-width-280(), ltr()) { 41 | margin-right: 10px; 42 | } 43 | @include query(max-width-280(), rtl()) { 44 | margin-left: 10px; 45 | } 46 | 47 | &.ended { 48 | cursor: default; 49 | } 50 | 51 | &:hover:not(.ended), 52 | &.active { 53 | background-color: $active-color; 54 | } 55 | } 56 | 57 | .clickable { 58 | cursor: pointer; 59 | } 60 | 61 | button.liveIndicatorButton { 62 | font-size: 12px; 63 | line-height: 14px; 64 | 65 | padding: 5px 6px; 66 | 67 | user-select: none; 68 | text-transform: uppercase !important; 69 | 70 | color: #fff; 71 | border: 0; 72 | border-radius: 0; 73 | outline: none; 74 | background-color: transparent; 75 | 76 | @include query(max-width-280()) { 77 | font-size: 10px; 78 | line-height: 12px; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/modules/ui/live-indicator/templates/index.ts: -------------------------------------------------------------------------------- 1 | import template from './live-indicator.dot'; 2 | const liveIndicatorTemplate = template.default ? template.default : template; 3 | export { liveIndicatorTemplate }; 4 | -------------------------------------------------------------------------------- /src/modules/ui/live-indicator/templates/live-indicator.dot: -------------------------------------------------------------------------------- 1 |
2 | 9 |
10 | -------------------------------------------------------------------------------- /src/modules/ui/live-indicator/types.ts: -------------------------------------------------------------------------------- 1 | import { ITooltipService } from '../core/tooltip/types'; 2 | import { ITextMap } from '../../text-map/types'; 3 | 4 | type ILiveIndicatorViewStyles = { 5 | liveIndicator: string; 6 | liveIndicatorButton: string; 7 | clickable: string; 8 | active: string; 9 | hidden: string; 10 | ended: string; 11 | }; 12 | 13 | type ILiveIndicatorViewCallbacks = { 14 | onClick: EventListenerOrEventListenerObject; 15 | }; 16 | 17 | type ILiveIndicatorViewConfig = { 18 | callbacks: ILiveIndicatorViewCallbacks; 19 | textMap: ITextMap; 20 | tooltipService: ITooltipService; 21 | }; 22 | 23 | interface ILiveIndicator { 24 | getElement(): HTMLElement; 25 | 26 | isHidden: boolean; 27 | isActive: boolean; 28 | 29 | show(): void; 30 | hide(): void; 31 | 32 | destroy(): void; 33 | } 34 | 35 | export { 36 | ILiveIndicator, 37 | ILiveIndicatorViewStyles, 38 | ILiveIndicatorViewCallbacks, 39 | ILiveIndicatorViewConfig, 40 | }; 41 | -------------------------------------------------------------------------------- /src/modules/ui/loader/loader.scss: -------------------------------------------------------------------------------- 1 | @import '../shared'; 2 | 3 | $diameter: 42px; 4 | $border-width: 3px; 5 | $outer-dim: $diameter + ($border-width * 2); 6 | $timing: 1s; 7 | 8 | 9 | .loader { 10 | position: absolute; 11 | z-index: 90; 12 | top: 50%; 13 | left: 50%; 14 | 15 | display: none; 16 | clip: rect(0, $outer-dim, $outer-dim, $outer-dim / 2); 17 | 18 | width: $diameter; 19 | height: $diameter; 20 | margin-top: - $diameter / 2; 21 | margin-left: - $diameter / 2; 22 | 23 | animation: rotate $timing linear infinite; 24 | 25 | color: white; 26 | 27 | &.active { 28 | display: block; 29 | } 30 | 31 | @mixin inner-circle() { 32 | position: absolute; 33 | top: 0; 34 | right: 0; 35 | bottom: 0; 36 | left: 0; 37 | 38 | content: ''; 39 | 40 | border: $border-width solid currentColor; 41 | border-radius: 50%; 42 | } 43 | 44 | &::after { 45 | clip: rect($border-width + 1px, $outer-dim, $outer-dim, $outer-dim / 2); 46 | 47 | animation: clip $timing linear infinite; 48 | 49 | @include inner-circle(); 50 | } 51 | &::before { 52 | clip: rect(0, $outer-dim, $outer-dim, $outer-dim / 2); 53 | 54 | animation: clip-reverse $timing linear infinite; 55 | 56 | @include inner-circle(); 57 | } 58 | 59 | @keyframes clip { 60 | 50% { 61 | clip: rect($diameter, $outer-dim, $outer-dim, $outer-dim / 2); 62 | 63 | animation-timing-function: ease-in-out; 64 | } 65 | } 66 | 67 | @keyframes clip-reverse { 68 | 50% { 69 | clip: rect(0, $outer-dim, $border-width * 3, $outer-dim / 2); 70 | 71 | transform: rotate(135deg); 72 | animation-timing-function: ease-in-out; 73 | } 74 | } 75 | 76 | @keyframes rotate { 77 | from { 78 | transform: rotate(0); 79 | animation-timing-function: ease-out; 80 | } 81 | 45% { 82 | transform: rotate(18deg); 83 | 84 | color: white; 85 | } 86 | 55% { 87 | transform: rotate(54deg); 88 | } 89 | to { 90 | transform: rotate(360deg); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/modules/ui/loader/loader.view.ts: -------------------------------------------------------------------------------- 1 | import { IView } from '../core/types'; 2 | import View from '../core/view'; 3 | 4 | import { loaderTemplate } from './templates'; 5 | 6 | import htmlToElement from '../core/htmlToElement'; 7 | 8 | import { ILoaderViewStyles } from './types'; 9 | 10 | import styles from './loader.scss'; 11 | 12 | class LoaderView extends View 13 | implements IView { 14 | private _$rootElement: HTMLElement; 15 | 16 | constructor() { 17 | super(); 18 | 19 | this._$rootElement = htmlToElement( 20 | loaderTemplate({ 21 | styles: this.styleNames, 22 | }), 23 | ); 24 | } 25 | 26 | getElement() { 27 | return this._$rootElement; 28 | } 29 | 30 | showContent() { 31 | this._$rootElement.classList.add(this.styleNames.active); 32 | } 33 | 34 | hideContent() { 35 | this._$rootElement.classList.remove(this.styleNames.active); 36 | } 37 | 38 | hide() { 39 | this._$rootElement.classList.add(this.styleNames.hidden); 40 | } 41 | 42 | show() { 43 | this._$rootElement.classList.remove(this.styleNames.hidden); 44 | } 45 | 46 | destroy() { 47 | if (this._$rootElement.parentNode) { 48 | this._$rootElement.parentNode.removeChild(this._$rootElement); 49 | } 50 | 51 | this._$rootElement = null; 52 | } 53 | } 54 | 55 | LoaderView.extendStyleNames(styles); 56 | 57 | export default LoaderView; 58 | -------------------------------------------------------------------------------- /src/modules/ui/loader/templates/index.ts: -------------------------------------------------------------------------------- 1 | import template from './loader.dot'; 2 | const loaderTemplate = template.default ? template.default : template; 3 | export { loaderTemplate }; 4 | -------------------------------------------------------------------------------- /src/modules/ui/loader/templates/loader.dot: -------------------------------------------------------------------------------- 1 |
6 |
7 | -------------------------------------------------------------------------------- /src/modules/ui/loader/types.ts: -------------------------------------------------------------------------------- 1 | type ILoaderViewStyles = { 2 | loader: string; 3 | active: string; 4 | hidden: string; 5 | }; 6 | 7 | interface ILoader { 8 | hide(): void; 9 | show(): void; 10 | getElement(): HTMLElement; 11 | stopDelayedShow(): void; 12 | isDelayedShowScheduled: Boolean; 13 | destroy(): void; 14 | } 15 | 16 | export { ILoaderViewStyles, ILoader }; 17 | -------------------------------------------------------------------------------- /src/modules/ui/loading-cover/loading-cover.scss: -------------------------------------------------------------------------------- 1 | .loadingCover { 2 | position: absolute; 3 | z-index: 70; 4 | top: 0; 5 | right: 0; 6 | bottom: 0; 7 | left: 0; 8 | 9 | display: flex; 10 | 11 | width: 100%; 12 | height: 100%; 13 | 14 | transition: opacity .3s, visibility .3s, transform .3s; 15 | 16 | background: black no-repeat center; 17 | background-size: contain; 18 | 19 | justify-content: center; 20 | align-items: center; 21 | 22 | &.hidden { 23 | animation: none; 24 | } 25 | } 26 | 27 | .hidden { 28 | display: none !important; 29 | visibility: hidden !important; 30 | 31 | opacity: 0 !important; 32 | } 33 | 34 | .loadingCoverImage { 35 | height: 100%; 36 | 37 | transition: opacity .6s, visibility .6s; 38 | } 39 | 40 | @keyframes trans { 41 | 30% { 42 | transform: perspective(1000px) rotate3d(0, 10, 0, 10deg); 43 | } 44 | 45 | 60% { 46 | transform: perspective(1000px) rotate3d(0, 10, 0, 10deg); 47 | } 48 | 49 | 100% { 50 | transform: perspective(1000px) rotate3d(0, 10, 10, 10deg); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/modules/ui/loading-cover/loading-cover.view.ts: -------------------------------------------------------------------------------- 1 | import View from '../core/view'; 2 | import { IView } from '../core/types'; 3 | 4 | import { ILoadingCoverViewStyles } from './types'; 5 | 6 | import { loadingCoverTemplate } from './templates'; 7 | 8 | import getElementByHook from '../core/getElementByHook'; 9 | import htmlToElement from '../core/htmlToElement'; 10 | 11 | import styles from './loading-cover.scss'; 12 | 13 | class LoadingCoverView extends View 14 | implements IView { 15 | private _$rootElement: HTMLElement; 16 | private _$image: HTMLElement; 17 | 18 | constructor() { 19 | super(); 20 | 21 | this._initDOM(); 22 | } 23 | 24 | getElement() { 25 | return this._$rootElement; 26 | } 27 | 28 | private _initDOM() { 29 | this._$rootElement = htmlToElement( 30 | loadingCoverTemplate({ 31 | styles: this.styleNames, 32 | }), 33 | ); 34 | 35 | this._$image = getElementByHook(this._$rootElement, 'loading-cover-image'); 36 | } 37 | 38 | hide() { 39 | this._$rootElement.classList.add(this.styleNames.hidden); 40 | } 41 | 42 | show() { 43 | this._$rootElement.classList.remove(this.styleNames.hidden); 44 | } 45 | 46 | setCover(url: string | boolean) { 47 | if (url && typeof url === 'string') { 48 | this._$image.classList.add(this.styleNames.hidden); 49 | 50 | const onImageLoad = () => { 51 | this._$image.classList.remove(this.styleNames.hidden); 52 | this._$image.removeEventListener('load', onImageLoad); 53 | }; 54 | 55 | this._$image.addEventListener('load', onImageLoad); 56 | this._$image.setAttribute('src', url); 57 | } 58 | } 59 | 60 | destroy() { 61 | if (this._$rootElement.parentNode) { 62 | this._$rootElement.parentNode.removeChild(this._$rootElement); 63 | } 64 | 65 | this._$rootElement = null; 66 | } 67 | } 68 | 69 | LoadingCoverView.extendStyleNames(styles); 70 | 71 | export default LoadingCoverView; 72 | -------------------------------------------------------------------------------- /src/modules/ui/loading-cover/templates/index.ts: -------------------------------------------------------------------------------- 1 | import template from './loading-cover.dot'; 2 | const loadingCoverTemplate = template.default ? template.default : template; 3 | export { loadingCoverTemplate }; 4 | -------------------------------------------------------------------------------- /src/modules/ui/loading-cover/templates/loading-cover.dot: -------------------------------------------------------------------------------- 1 |
6 | 10 |
11 | -------------------------------------------------------------------------------- /src/modules/ui/loading-cover/types.ts: -------------------------------------------------------------------------------- 1 | type ILoadingCoverViewStyles = { 2 | loadingCover: string; 3 | loadingCoverImage: string; 4 | hidden: string; 5 | }; 6 | 7 | interface ILoadingCover { 8 | getElement(): HTMLElement; 9 | 10 | setLoadingCover(src: string): void; 11 | 12 | show(): void; 13 | hide(): void; 14 | 15 | destroy(): void; 16 | } 17 | 18 | export { ILoadingCover, ILoadingCoverViewStyles }; 19 | -------------------------------------------------------------------------------- /src/modules/ui/main-ui-block/main-ui-block.scss: -------------------------------------------------------------------------------- 1 | .mainUiBlock { 2 | position: absolute; 3 | top: 0; 4 | right: 0; 5 | bottom: 0; 6 | left: 0; 7 | 8 | display: flex; 9 | flex-direction: column; 10 | 11 | .tooltipContainerWrapper { 12 | position: relative; 13 | 14 | flex-grow: 2; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/modules/ui/main-ui-block/main-ui-block.view.ts: -------------------------------------------------------------------------------- 1 | import View from '../core/view'; 2 | import { IView } from '../core/types'; 3 | 4 | import { mainUIBlockTemplate } from './templates'; 5 | import htmlToElement from '../core/htmlToElement'; 6 | import styles from './main-ui-block.scss'; 7 | 8 | import { 9 | IMainUIBlockViewStyles, 10 | IMainUIBlockViewConfig, 11 | IMainUIBlockViewElements, 12 | } from './types'; 13 | 14 | class MainUIBlockView extends View 15 | implements IView { 16 | private _$rootElement: HTMLElement; 17 | 18 | constructor(config: IMainUIBlockViewConfig) { 19 | super(); 20 | 21 | this._initDOM(config.elements); 22 | } 23 | 24 | private _initDOM(elements: IMainUIBlockViewElements) { 25 | this._$rootElement = htmlToElement( 26 | mainUIBlockTemplate({ 27 | styles: this.styleNames, 28 | }), 29 | ); 30 | 31 | const $tooltipContainerWrapper = document.createElement('div'); 32 | $tooltipContainerWrapper.classList.add( 33 | this.styleNames.tooltipContainerWrapper, 34 | ); 35 | $tooltipContainerWrapper.appendChild(elements.tooltipContainer); 36 | 37 | this._$rootElement.appendChild(elements.topBlock); 38 | this._$rootElement.appendChild($tooltipContainerWrapper); 39 | this._$rootElement.appendChild(elements.bottomBlock); 40 | } 41 | 42 | getElement() { 43 | return this._$rootElement; 44 | } 45 | 46 | destroy() { 47 | if (this._$rootElement.parentNode) { 48 | this._$rootElement.parentNode.removeChild(this._$rootElement); 49 | } 50 | 51 | this._$rootElement = null; 52 | } 53 | } 54 | 55 | MainUIBlockView.extendStyleNames(styles); 56 | 57 | export { IMainUIBlockViewConfig }; 58 | 59 | export default MainUIBlockView; 60 | -------------------------------------------------------------------------------- /src/modules/ui/main-ui-block/templates/index.ts: -------------------------------------------------------------------------------- 1 | import template from './mainUIBlock.dot'; 2 | const mainUIBlockTemplate = template.default ? template.default : template; 3 | export { mainUIBlockTemplate }; 4 | -------------------------------------------------------------------------------- /src/modules/ui/main-ui-block/templates/mainUIBlock.dot: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /src/modules/ui/main-ui-block/types.ts: -------------------------------------------------------------------------------- 1 | type IMainUIBlockViewStyles = { 2 | mainUiBlock: string; 3 | tooltipContainerWrapper: string; 4 | hidden: string; 5 | }; 6 | 7 | type IMainUIBlockViewElements = { 8 | tooltipContainer: HTMLElement; 9 | topBlock: HTMLElement; 10 | bottomBlock: HTMLElement; 11 | }; 12 | 13 | type IMainUIBlockViewConfig = { 14 | elements: IMainUIBlockViewElements; 15 | }; 16 | 17 | interface IMainUIBlock { 18 | getElement(): HTMLElement; 19 | 20 | enableShowingContent(): void; 21 | disableShowingContent(): void; 22 | 23 | show(): void; 24 | hide(): void; 25 | 26 | setShouldAlwaysShow(flag: boolean): void; 27 | destroy(): void; 28 | } 29 | 30 | interface IMainUIBlockAPI { 31 | showMainUI?(): void; 32 | hideMainUI?(): void; 33 | setMainUIShouldAlwaysShow?(flag: boolean): void; 34 | } 35 | 36 | export { 37 | IMainUIBlockAPI, 38 | IMainUIBlock, 39 | IMainUIBlockViewStyles, 40 | IMainUIBlockViewElements, 41 | IMainUIBlockViewConfig, 42 | }; 43 | -------------------------------------------------------------------------------- /src/modules/ui/overlay/overlay.scss: -------------------------------------------------------------------------------- 1 | @import '../shared'; 2 | 3 | .overlay { 4 | position: absolute; 5 | z-index: 100; 6 | top: 0; 7 | right: 0; 8 | bottom: 0; 9 | left: 0; 10 | 11 | display: none; 12 | 13 | &.active { 14 | display: flex; 15 | 16 | justify-content: center; 17 | align-items: center; 18 | } 19 | } 20 | 21 | .poster { 22 | position: absolute; 23 | top: 0; 24 | right: 0; 25 | bottom: 0; 26 | left: 0; 27 | 28 | width: 100%; 29 | height: 100%; 30 | 31 | background: black no-repeat center; 32 | background-size: cover; 33 | 34 | justify-content: center; 35 | align-items: center; 36 | } 37 | 38 | .poster:before { 39 | position: absolute; 40 | top: 0; 41 | left: 0; 42 | 43 | width: 100%; 44 | height: 100%; 45 | 46 | content: ''; 47 | 48 | background-color: rgba(0, 0, 0, .35); 49 | } 50 | 51 | .icon { 52 | position: relative; 53 | 54 | width: 71px; 55 | height: 71px; 56 | 57 | cursor: pointer; 58 | 59 | opacity: 1; 60 | 61 | @include query(max-width-550()) { 62 | width: 54px; 63 | height: 54px; 64 | } 65 | @include query(max-width-400()) { 66 | width: 36px; 67 | height: 36px; 68 | } 69 | 70 | &:hover { 71 | opacity: .8; 72 | } 73 | } 74 | 75 | .transparency { 76 | background: transparent; 77 | } 78 | -------------------------------------------------------------------------------- /src/modules/ui/overlay/overlay.theme.ts: -------------------------------------------------------------------------------- 1 | import { IThemeConfig } from '../core/theme/types'; 2 | 3 | export default { 4 | overlayPlaySvgFill: { 5 | fill: (data: IThemeConfig) => data.color, 6 | }, 7 | overlayPlaySvgStroke: { 8 | stroke: (data: IThemeConfig) => data.color, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /src/modules/ui/overlay/templates/index.ts: -------------------------------------------------------------------------------- 1 | import template from './overlay.dot'; 2 | const overlayTemplate = template.default ? template.default : template; 3 | export { overlayTemplate }; 4 | -------------------------------------------------------------------------------- /src/modules/ui/overlay/templates/overlay.dot: -------------------------------------------------------------------------------- 1 |
6 |
10 |
11 |
15 | 22 | 23 | 27 | 34 | 38 | 39 | 40 |
41 |
42 | -------------------------------------------------------------------------------- /src/modules/ui/overlay/types.ts: -------------------------------------------------------------------------------- 1 | import { IThemeService } from '../core/theme'; 2 | 3 | type IOverlayViewStyles = { 4 | overlay: string; 5 | poster: string; 6 | active: string; 7 | hidden: string; 8 | transparency: string; 9 | }; 10 | 11 | type IOverlayViewCallbacks = { 12 | onPlayClick: EventListenerOrEventListenerObject; 13 | }; 14 | 15 | type IOverlayViewConfig = { 16 | callbacks: IOverlayViewCallbacks; 17 | theme: IThemeService; 18 | }; 19 | 20 | interface IOverlay { 21 | getElement(): HTMLElement; 22 | 23 | show(): void; 24 | hide(): void; 25 | 26 | setPoster(src: string): void; 27 | 28 | destroy(): void; 29 | } 30 | 31 | interface IOverlayAPI { 32 | showOverlay?(): void; 33 | hideOverlay?(): void; 34 | setPoster?(src: string): void; 35 | } 36 | 37 | export { 38 | IOverlayAPI, 39 | IOverlay, 40 | IOverlayViewStyles, 41 | IOverlayViewCallbacks, 42 | IOverlayViewConfig, 43 | }; 44 | -------------------------------------------------------------------------------- /src/modules/ui/preview-full-size/preview-full-size.scss: -------------------------------------------------------------------------------- 1 | @import '../shared'; 2 | 3 | .container { 4 | position: absolute; 5 | z-index: 55; 6 | top: 0; 7 | right: 0; 8 | bottom: 0; 9 | left: 0; 10 | 11 | display: flex; 12 | flex-direction: column-reverse; 13 | 14 | pointer-events: none; 15 | 16 | background-color: black; 17 | 18 | align-items: center; 19 | } 20 | 21 | .frame { 22 | position: absolute; 23 | z-index: 2; 24 | top: 0; 25 | right: 0; 26 | bottom: 0; 27 | left: 0; 28 | 29 | opacity: .5; 30 | } 31 | -------------------------------------------------------------------------------- /src/modules/ui/preview-full-size/preview-full-size.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IPreviewService, 3 | INormalizedFramesQuality, 4 | } from '../preview-service/types'; 5 | import { IRootContainer } from '../../root-container/types'; 6 | 7 | import { IPreviewFullSize } from './types'; 8 | 9 | import PreviewFullsizeView from './preview-full-size.view'; 10 | 11 | export default class PreviewFullsize implements IPreviewFullSize { 12 | static moduleName = 'previewFullSize'; 13 | static View = PreviewFullsizeView; 14 | static dependencies = ['previewService', 'rootContainer']; 15 | 16 | private _previewService: IPreviewService; 17 | 18 | private _currentFrame: INormalizedFramesQuality; 19 | 20 | view: PreviewFullsizeView; 21 | 22 | constructor({ 23 | previewService, 24 | rootContainer, 25 | }: { 26 | previewService: IPreviewService; 27 | rootContainer: IRootContainer; 28 | }) { 29 | this._previewService = previewService; 30 | 31 | this._initUI(); 32 | 33 | this.hide(); 34 | rootContainer.appendComponentElement(this.getElement()); 35 | } 36 | 37 | private _initUI() { 38 | this.view = new PreviewFullsize.View(); 39 | } 40 | 41 | getElement(): HTMLElement { 42 | return this.view.getElement(); 43 | } 44 | 45 | showAt(second: number) { 46 | this.view.show(); 47 | const framesData: INormalizedFramesQuality[] = this._previewService.getAt( 48 | second, 49 | ); 50 | 51 | if (!framesData) { 52 | this.view.clear(); 53 | return; 54 | } 55 | 56 | const frameData = framesData.pop(); 57 | 58 | if (this._currentFrame) { 59 | if (this._currentFrame.spriteUrl !== frameData.spriteUrl) { 60 | this.view.clear(); 61 | } 62 | } 63 | 64 | this.view.setPreview(frameData); 65 | 66 | this._currentFrame = frameData; 67 | } 68 | 69 | hide() { 70 | this.view.hide(); 71 | } 72 | 73 | destroy(): void { 74 | this.view.destroy(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/modules/ui/preview-full-size/templates/index.ts: -------------------------------------------------------------------------------- 1 | import template from './preview.dot'; 2 | const previewTemplate = template.default ? template.default : template; 3 | export { previewTemplate }; 4 | -------------------------------------------------------------------------------- /src/modules/ui/preview-full-size/templates/preview.dot: -------------------------------------------------------------------------------- 1 |
2 |
4 |
5 |
6 | -------------------------------------------------------------------------------- /src/modules/ui/preview-full-size/types.ts: -------------------------------------------------------------------------------- 1 | interface IPreviewFullSize { 2 | getElement(): HTMLElement; 3 | 4 | showAt(second: number): void; 5 | hide(): void; 6 | 7 | destroy(): void; 8 | } 9 | 10 | interface IPreviewFullSizeViewStyles { 11 | container: string; 12 | frame: string; 13 | hidden: string; 14 | } 15 | 16 | export { IPreviewFullSizeViewStyles, IPreviewFullSize }; 17 | -------------------------------------------------------------------------------- /src/modules/ui/preview-service/adapter.ts: -------------------------------------------------------------------------------- 1 | import { IFramesData, IFramesQuality, INormalizedFramesQuality } from './types'; 2 | 3 | function normalizeFramesQuality( 4 | framesCount: number, 5 | quality: IFramesQuality, 6 | neededFrame: number, 7 | ): INormalizedFramesQuality { 8 | const framesInSprite = 9 | quality.framesInSprite.vert * quality.framesInSprite.horz; 10 | const frameNumberInSprite = neededFrame % framesInSprite; 11 | const spriteNumber = Math.floor(neededFrame / framesInSprite); 12 | const horzPositionInSprite = 13 | frameNumberInSprite % quality.framesInSprite.horz; 14 | const vertPositionInSprite = Math.floor( 15 | frameNumberInSprite / quality.framesInSprite.vert, 16 | ); 17 | 18 | const url = quality.spriteUrlMask.replace('%d', spriteNumber.toString()); 19 | 20 | return { 21 | frameSize: quality.frameSize, 22 | framesInSprite: 23 | (spriteNumber + 1) * framesInSprite <= framesCount 24 | ? quality.framesInSprite 25 | : { 26 | horz: quality.framesInSprite.horz, 27 | vert: Math.ceil( 28 | (framesCount % framesInSprite) / quality.framesInSprite.vert, 29 | ), 30 | }, 31 | framePositionInSprite: { 32 | vert: vertPositionInSprite, 33 | horz: horzPositionInSprite, 34 | }, 35 | spriteUrl: url, 36 | }; 37 | } 38 | 39 | function getAt( 40 | data: IFramesData, 41 | second: number, 42 | duration: number, 43 | ): INormalizedFramesQuality[] { 44 | const { framesCount } = data; 45 | const neededFrame = Math.floor((second * framesCount) / duration); 46 | return data.qualities.map(quality => 47 | normalizeFramesQuality(framesCount, quality, neededFrame), 48 | ); 49 | } 50 | 51 | export { getAt }; 52 | -------------------------------------------------------------------------------- /src/modules/ui/preview-service/preview-service.ts: -------------------------------------------------------------------------------- 1 | import playerAPI from '../../../core/player-api-decorator'; 2 | 3 | import { IPlaybackEngine } from '../../playback-engine/types'; 4 | 5 | import { getAt } from './adapter'; 6 | import { 7 | IPreviewAPI, 8 | IPreviewService, 9 | IFramesData, 10 | INormalizedFramesQuality, 11 | } from './types'; 12 | 13 | class PreviewService implements IPreviewService { 14 | static moduleName = 'previewService'; 15 | static dependencies = ['engine']; 16 | 17 | private _engine: IPlaybackEngine; 18 | 19 | private _framesMap: IFramesData; 20 | 21 | constructor({ engine }: { engine: IPlaybackEngine }) { 22 | this._engine = engine; 23 | } 24 | 25 | @playerAPI() 26 | setFramesMap(map: IFramesData) { 27 | this._framesMap = map; 28 | } 29 | 30 | getAt(second: number): INormalizedFramesQuality[] { 31 | if (!this._framesMap) { 32 | return; 33 | } 34 | 35 | const duration = this._engine.getDuration(); 36 | if (!duration) { 37 | return; 38 | } 39 | 40 | return getAt(this._framesMap, second, duration); 41 | } 42 | 43 | destroy(): void { 44 | this._framesMap = null; 45 | } 46 | } 47 | 48 | export { IPreviewAPI }; 49 | export default PreviewService; 50 | -------------------------------------------------------------------------------- /src/modules/ui/preview-service/types.ts: -------------------------------------------------------------------------------- 1 | interface IFramesData { 2 | framesCount: number; 3 | qualities: IFramesQuality[]; 4 | } 5 | 6 | interface IFramesQuality { 7 | spriteUrlMask: string; 8 | frameSize: IFrameSize; 9 | framesInSprite: IMaxFramesInSprite; 10 | } 11 | 12 | interface IMaxFramesInSprite { 13 | vert: number; 14 | horz: number; 15 | } 16 | 17 | interface IFramePositionInSprite { 18 | vert: number; 19 | horz: number; 20 | } 21 | 22 | interface ITotalFramesInSprite { 23 | vert: number; 24 | horz: number; 25 | } 26 | 27 | interface IFrameSize { 28 | width: number; 29 | height: number; 30 | } 31 | 32 | interface INormalizedFramesQuality { 33 | spriteUrl: string; 34 | framePositionInSprite: IFramePositionInSprite; 35 | frameSize: IFrameSize; 36 | framesInSprite: ITotalFramesInSprite; 37 | } 38 | 39 | interface IPreviewService { 40 | setFramesMap(map: IFramesData): void; 41 | getAt(second: number): INormalizedFramesQuality[]; 42 | 43 | destroy(): void; 44 | } 45 | 46 | interface IPreviewAPI { 47 | setFramesMap?(map: IFramesData): void; 48 | } 49 | 50 | export { 51 | IPreviewAPI, 52 | IPreviewService, 53 | IFramesData, 54 | IFramesQuality, 55 | INormalizedFramesQuality, 56 | }; 57 | -------------------------------------------------------------------------------- /src/modules/ui/preview-thumbnail/preview-thumbnail.scss: -------------------------------------------------------------------------------- 1 | $thumbnail-width: 180px; 2 | $thumbnail-height: 90px; 3 | $thumbnail-border-size: 2px; 4 | 5 | .container { 6 | display: flex; 7 | flex-direction: column-reverse; 8 | 9 | width: $thumbnail-width; 10 | height: $thumbnail-height; 11 | 12 | border: $thumbnail-border-size solid rgba(0, 0, 0, 0.5); 13 | border-radius: 2px; 14 | background-color: rgba(0, 0, 0, 0.5); 15 | 16 | align-items: center; 17 | 18 | &.empty { 19 | width: initial; 20 | height: initial; 21 | 22 | border: none; 23 | border-radius: 0; 24 | } 25 | } 26 | 27 | .highQualityFrame, 28 | .lowQualityFrame { 29 | position: absolute; 30 | top: $thumbnail-border-size; 31 | right: $thumbnail-border-size; 32 | bottom: $thumbnail-border-size; 33 | left: $thumbnail-border-size; 34 | 35 | width: $thumbnail-width; 36 | height: $thumbnail-height; 37 | } 38 | 39 | .highQualityFrame { 40 | z-index: 2; 41 | } 42 | 43 | .lowQualityFrame { 44 | z-index: 1; 45 | } 46 | 47 | .empty { 48 | .thumbText { 49 | background: none; 50 | } 51 | 52 | .highQualityFrame, 53 | .lowQualityFrame { 54 | width: 0; 55 | height: 0; 56 | } 57 | } 58 | 59 | .thumbText { 60 | font-size: 11px; 61 | line-height: 12px; 62 | 63 | position: relative; 64 | z-index: 3; 65 | 66 | padding: 4px 5px; 67 | 68 | white-space: nowrap; 69 | 70 | color: white; 71 | background: rgba(0, 0, 0, 0.8); 72 | } 73 | -------------------------------------------------------------------------------- /src/modules/ui/preview-thumbnail/preview-thumbnail.ts: -------------------------------------------------------------------------------- 1 | import PreviewThumbnailView from './preview-thumbnail.view'; 2 | 3 | import { 4 | IPreviewService, 5 | INormalizedFramesQuality, 6 | } from '../preview-service/types'; 7 | 8 | import { IPreviewThumbnail } from './types'; 9 | 10 | export default class PreviewThumbnail implements IPreviewThumbnail { 11 | static moduleName = 'previewThumbnail'; 12 | static View = PreviewThumbnailView; 13 | static dependencies = ['previewService']; 14 | 15 | private _previewService: IPreviewService; 16 | 17 | private _currentFrames: INormalizedFramesQuality[]; 18 | 19 | view: PreviewThumbnailView; 20 | 21 | constructor({ previewService }: { previewService: IPreviewService }) { 22 | this._previewService = previewService; 23 | 24 | this._initUI(); 25 | } 26 | 27 | private _initUI() { 28 | this.view = new PreviewThumbnail.View(); 29 | } 30 | 31 | getElement(): HTMLElement { 32 | return this.view.getElement(); 33 | } 34 | 35 | showAt(second: number) { 36 | const config: INormalizedFramesQuality[] = this._previewService.getAt( 37 | second, 38 | ); 39 | 40 | if (!config) { 41 | this.view.showAsEmpty(); 42 | return; 43 | } 44 | 45 | this.view.showWithPreview(); 46 | 47 | if (this._currentFrames) { 48 | if (this._currentFrames[0].spriteUrl !== config[0].spriteUrl) { 49 | this.view.clearLowQualityPreview(); 50 | } 51 | if (this._currentFrames[1].spriteUrl !== config[1].spriteUrl) { 52 | this.view.clearHighQualityPreview(); 53 | } 54 | } 55 | 56 | this.view.setLowQualityPreview(config[0]); 57 | this.view.setHighQualityPreview(config[1]); 58 | 59 | this._currentFrames = config; 60 | } 61 | 62 | setTime(time: string) { 63 | this.view.setTime(time); 64 | } 65 | 66 | destroy(): void { 67 | this.view.destroy(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/modules/ui/preview-thumbnail/templates/index.ts: -------------------------------------------------------------------------------- 1 | import template from './thumbnail.dot'; 2 | const thumbnailTemplate = template.default ? template.default : template; 3 | export { thumbnailTemplate }; 4 | -------------------------------------------------------------------------------- /src/modules/ui/preview-thumbnail/templates/thumbnail.dot: -------------------------------------------------------------------------------- 1 |
2 |
4 |
5 |
7 |
8 |
10 |
11 |
12 | -------------------------------------------------------------------------------- /src/modules/ui/preview-thumbnail/types.ts: -------------------------------------------------------------------------------- 1 | interface IPreviewThumbnail { 2 | getElement(): HTMLElement; 3 | 4 | showAt(second: number): void; 5 | setTime(time: string): void; 6 | 7 | destroy(): void; 8 | } 9 | 10 | interface IPreviewThumbnailViewStyles { 11 | container: string; 12 | highQualityFrame: string; 13 | lowQualityFrame: string; 14 | thumbText: string; 15 | empty: string; 16 | } 17 | 18 | export { IPreviewThumbnailViewStyles, IPreviewThumbnail }; 19 | -------------------------------------------------------------------------------- /src/modules/ui/screen/screen.scss: -------------------------------------------------------------------------------- 1 | .screen { 2 | position: absolute; 3 | z-index: 50; 4 | top: 0; 5 | right: 0; 6 | bottom: 0; 7 | left: 0; 8 | 9 | display: flex; 10 | flex-direction: column; 11 | 12 | width: 100%; 13 | height: 100%; 14 | 15 | background-color: black; 16 | 17 | justify-content: center; 18 | align-items: center; 19 | 20 | &.regularMode, 21 | &.blurMode { 22 | video { 23 | width: 100%; 24 | height: 100%; 25 | } 26 | } 27 | 28 | &.fillMode { 29 | video { 30 | position: absolute; 31 | } 32 | } 33 | 34 | &.verticalStripes { 35 | &.fillMode { 36 | video { 37 | width: 100%; 38 | height: auto !important; 39 | } 40 | } 41 | } 42 | 43 | &.horizontalStripes { 44 | &.fillMode { 45 | video { 46 | height: 100%; 47 | } 48 | } 49 | } 50 | 51 | video { 52 | position: relative; 53 | z-index: 1; 54 | 55 | box-shadow: 0 0 20px rgba(0, 0, 0, .2); 56 | } 57 | 58 | &.hiddenCursor { 59 | cursor: none; 60 | } 61 | } 62 | 63 | .backgroundCanvas { 64 | position: absolute; 65 | z-index: 0; 66 | top: 0; 67 | right: 0; 68 | bottom: 0; 69 | left: 0; 70 | 71 | filter: blur(14px); 72 | } 73 | -------------------------------------------------------------------------------- /src/modules/ui/screen/templates/index.ts: -------------------------------------------------------------------------------- 1 | import template from './screen.dot'; 2 | const screenTemplate = template.default ? template.default : template; 3 | export { screenTemplate }; 4 | -------------------------------------------------------------------------------- /src/modules/ui/screen/templates/screen.dot: -------------------------------------------------------------------------------- 1 |
5 | 8 |
9 | -------------------------------------------------------------------------------- /src/modules/ui/screen/types.ts: -------------------------------------------------------------------------------- 1 | type IScreenViewStyles = { 2 | screen: string; 3 | screenTopBackground: string; 4 | screenBottomBackground: string; 5 | hidden: string; 6 | visible: string; 7 | hiddenCursor: string; 8 | horizontalStripes: string; 9 | verticalStripes: string; 10 | fillMode: string; 11 | blurMode: string; 12 | regularMode: string; 13 | }; 14 | 15 | type IScreenViewCallbacks = { 16 | onWrapperMouseClick: EventListenerOrEventListenerObject; 17 | onWrapperMouseDblClick: EventListenerOrEventListenerObject; 18 | }; 19 | 20 | type IScreenViewConfig = { 21 | callbacks: IScreenViewCallbacks; 22 | playbackViewElement: HTMLElement; 23 | nativeControls: boolean; 24 | }; 25 | 26 | enum VideoViewMode { 27 | REGULAR = 'REGULAR', 28 | BLUR = 'BLUR', 29 | FILL = 'FILL', 30 | } 31 | 32 | interface IScreen { 33 | getElement(): HTMLElement; 34 | showCursor(): void; 35 | hideCursor(): void; 36 | 37 | show(): void; 38 | hide(): void; 39 | 40 | setVideoViewMode(viewMode: VideoViewMode): void; 41 | 42 | destroy(): void; 43 | } 44 | 45 | interface IScreenAPI { 46 | setVideoViewMode?(viewMode: VideoViewMode): void; 47 | } 48 | 49 | export { 50 | IScreenAPI, 51 | IScreen, 52 | VideoViewMode, 53 | IScreenViewStyles, 54 | IScreenViewCallbacks, 55 | IScreenViewConfig, 56 | }; 57 | -------------------------------------------------------------------------------- /src/modules/ui/shared.scss: -------------------------------------------------------------------------------- 1 | @import 'conditions'; 2 | 3 | .controlButton { 4 | display: flex; 5 | 6 | padding: 0; 7 | 8 | cursor: pointer; 9 | transition-duration: .2s; 10 | transition-property: opacity; 11 | 12 | opacity: 1; 13 | border: 0; 14 | border-radius: 0; 15 | outline: none; 16 | background-color: transparent; 17 | 18 | justify-content: center; 19 | align-items: center; 20 | 21 | &:hover { 22 | opacity: .7; 23 | } 24 | } 25 | 26 | @mixin hidden() { 27 | visibility: hidden !important; 28 | 29 | width: 0 !important; 30 | min-width: 0 !important; 31 | height: 0 !important; 32 | min-height: 0 !important; 33 | margin: 0 !important; 34 | padding: 0 !important; 35 | 36 | opacity: 0 !important; 37 | } 38 | 39 | .hidden { 40 | @include hidden(); 41 | } 42 | -------------------------------------------------------------------------------- /src/modules/ui/subtitles/subtitles.scss: -------------------------------------------------------------------------------- 1 | @import '../shared'; 2 | 3 | .container { 4 | position: absolute; 5 | z-index: 70; 6 | top: 0; 7 | right: 0; 8 | bottom: 0; 9 | left: 0; 10 | 11 | display: flex; 12 | 13 | transition: .2s bottom; 14 | pointer-events: none; 15 | 16 | justify-content: center; 17 | align-items: flex-end; 18 | } 19 | 20 | .subtitlesContainer { 21 | display: flex; 22 | flex-direction: column; 23 | 24 | padding: 5px; 25 | 26 | transition: .2s margin; 27 | pointer-events: all; 28 | 29 | justify-content: center; 30 | 31 | &.controlsShown { 32 | margin-bottom: 60px; 33 | 34 | @include query(in-full-screen()) { 35 | margin-bottom: 100px; 36 | } 37 | } 38 | } 39 | 40 | .subtitle { 41 | font-size: 16px; 42 | line-height: 16px; 43 | 44 | padding: 5px; 45 | 46 | text-align: center; 47 | 48 | color: white; 49 | border-radius: 2px; 50 | background: rgba(50, 50, 50, .85); 51 | 52 | @include query(in-full-screen()) { 53 | font-size: 24px; 54 | line-height: 24px; 55 | 56 | padding: 8px; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/modules/ui/subtitles/templates/index.ts: -------------------------------------------------------------------------------- 1 | import template1 from './subtitles.dot'; 2 | import template2 from './subtitle.dot'; 3 | const subtitlesTemplate = template1.default ? template1.default : template1; 4 | const singleSubtitleTemplate = template2.default 5 | ? template2.default 6 | : template2; 7 | export { subtitlesTemplate, singleSubtitleTemplate }; 8 | -------------------------------------------------------------------------------- /src/modules/ui/subtitles/templates/subtitle.dot: -------------------------------------------------------------------------------- 1 |
2 |
3 | -------------------------------------------------------------------------------- /src/modules/ui/subtitles/templates/subtitles.dot: -------------------------------------------------------------------------------- 1 |
2 |
4 |
5 |
6 | -------------------------------------------------------------------------------- /src/modules/ui/subtitles/types.ts: -------------------------------------------------------------------------------- 1 | interface ISubtitleConfig { 2 | src: string; 3 | lang?: string; 4 | label?: string; 5 | } 6 | 7 | type ISubtitlesViewStyles = { 8 | container: string; 9 | subtitlesContainer: string; 10 | subtitle: string; 11 | controlsShown: string; 12 | hidden: string; 13 | }; 14 | 15 | interface ISubtitles { 16 | setSubtitles( 17 | subtitles: string | ISubtitleConfig | Array, 18 | ): void; 19 | 20 | setActiveSubtitle(index: number): void; 21 | 22 | showSubtitles(): void; 23 | hideSubtitles(): void; 24 | removeSubtitles(): void; 25 | 26 | destroy(): void; 27 | } 28 | 29 | interface ISubtitlesAPI { 30 | setSubtitles?( 31 | subtitles: string | ISubtitleConfig | Array, 32 | ): void; 33 | 34 | setActiveSubtitle?(index: number): void; 35 | 36 | showSubtitles?(): void; 37 | hideSubtitles?(): void; 38 | removeSubtitles?(): void; 39 | } 40 | 41 | export { ISubtitlesAPI, ISubtitles, ISubtitleConfig, ISubtitlesViewStyles }; 42 | -------------------------------------------------------------------------------- /src/modules/ui/title/templates/index.ts: -------------------------------------------------------------------------------- 1 | import template from './title.dot'; 2 | const titleTemplate = template.default ? template.default : template; 3 | export { titleTemplate }; 4 | -------------------------------------------------------------------------------- /src/modules/ui/title/templates/title.dot: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /src/modules/ui/title/title.scss: -------------------------------------------------------------------------------- 1 | @import '../shared'; 2 | 3 | .title { 4 | font-size: 16px; 5 | line-height: 17px; 6 | 7 | overflow: hidden; 8 | 9 | white-space: nowrap; 10 | text-overflow: ellipsis; 11 | 12 | @include query(max-width-550()) { 13 | font-size: 14px; 14 | line-height: 15px; 15 | } 16 | @include query(max-width-300()) { 17 | font-size: 12px; 18 | line-height: 13px; 19 | } 20 | @include query(in-full-screen()) { 21 | font-size: 20px; 22 | line-height: 20px; 23 | } 24 | 25 | &.link { 26 | cursor: pointer; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/modules/ui/title/title.theme.ts: -------------------------------------------------------------------------------- 1 | import { IThemeConfig } from '../core/theme/types'; 2 | 3 | export default { 4 | titleText: { 5 | color: (data: IThemeConfig) => data.color, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /src/modules/ui/title/types.ts: -------------------------------------------------------------------------------- 1 | import { IThemeService } from '../core/theme'; 2 | 3 | type ITitleViewStyles = { 4 | title: string; 5 | link: string; 6 | hidden: string; 7 | }; 8 | 9 | type ITitleViewCallbacks = { 10 | onClick: EventListenerOrEventListenerObject; 11 | }; 12 | 13 | type ITitleViewConfig = { 14 | callbacks: ITitleViewCallbacks; 15 | theme: IThemeService; 16 | }; 17 | 18 | interface ITitle { 19 | getElement(): HTMLElement; 20 | setTitle(title?: string): void; 21 | setTitleClickCallback(callback?: () => void): void; 22 | 23 | show(): void; 24 | hide(): void; 25 | 26 | destroy(): void; 27 | } 28 | 29 | interface ITitleAPI { 30 | setTitle?(title?: string): void; 31 | setTitleClickCallback?(callback?: () => void): void; 32 | } 33 | 34 | export { 35 | ITitleAPI, 36 | ITitle, 37 | ITitleViewStyles, 38 | ITitleViewCallbacks, 39 | ITitleViewConfig, 40 | }; 41 | -------------------------------------------------------------------------------- /src/modules/ui/top-block/templates/index.ts: -------------------------------------------------------------------------------- 1 | import template from './top-block.dot'; 2 | const topBlockTemplate = template.default ? template.default : template; 3 | export { topBlockTemplate }; 4 | -------------------------------------------------------------------------------- /src/modules/ui/top-block/templates/top-block.dot: -------------------------------------------------------------------------------- 1 |
3 |
5 |
6 |
7 |
9 |
10 |
12 |
13 |
14 |
15 | -------------------------------------------------------------------------------- /src/modules/ui/top-block/types.ts: -------------------------------------------------------------------------------- 1 | type ITopBlockViewStyles = { 2 | topBlock: string; 3 | elementsContainer: string; 4 | titleContainer: string; 5 | liveIndicatorContainer: string; 6 | activated: string; 7 | hidden: string; 8 | }; 9 | 10 | type ITopBlockViewCallbacks = { 11 | onBlockMouseMove: EventListenerOrEventListenerObject; 12 | onBlockMouseOut: EventListenerOrEventListenerObject; 13 | }; 14 | 15 | type ITopBlockViewElements = { 16 | title: HTMLElement; 17 | liveIndicator: HTMLElement; 18 | }; 19 | 20 | type ITopBlockViewConfig = { 21 | elements: ITopBlockViewElements; 22 | callbacks: ITopBlockViewCallbacks; 23 | }; 24 | 25 | interface ITopBlock { 26 | getElement(): HTMLElement; 27 | isFocused: boolean; 28 | 29 | show(): void; 30 | hide(): void; 31 | 32 | showTitle(): void; 33 | hideTitle(): void; 34 | showLiveIndicator(): void; 35 | hideLiveIndicator(): void; 36 | 37 | showContent(): void; 38 | hideContent(): void; 39 | 40 | destroy(): void; 41 | } 42 | 43 | interface ITopBlockAPI { 44 | showTitle?(): void; 45 | hideTitle?(): void; 46 | showLiveIndicator?(): void; 47 | hideLiveIndicator?(): void; 48 | } 49 | 50 | export { 51 | ITopBlockAPI, 52 | ITopBlock, 53 | ITopBlockViewStyles, 54 | ITopBlockViewElements, 55 | ITopBlockViewCallbacks, 56 | ITopBlockViewConfig, 57 | }; 58 | -------------------------------------------------------------------------------- /src/stories/constants.ts: -------------------------------------------------------------------------------- 1 | export const RGB_HEX = require('rgb-hex'); 2 | 3 | export const DEFAULT_URLS: any = { 4 | DASH: 'https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd', 5 | HLS: 6 | 'https://files.wixstatic.com/files/video/64b2fa_039e5c16db504dbaad166ba28d377744/repackage/hls', 7 | MP4: 8 | 'https://storage.googleapis.com/video-player-media-server-static/test2.mp4', 9 | 'MP4-VERTICAL': 10 | 'https://storage.googleapis.com/video-player-media-server-static/videoplayback.mp4', 11 | LIVE: 12 | 'https://video-player-media-server-dot-wixgamma.appspot.com/live/stream/manifest.m3u8', 13 | }; 14 | 15 | export const DEFAUTL_CONFIG = { 16 | framesCount: 178, 17 | qualities: [ 18 | { 19 | spriteUrlMask: 20 | 'https://storage.googleapis.com/video-player-media-server-static/thumbnails/low_rez_sprite_%d.jpg', 21 | frameSize: { width: 90, height: 45 }, 22 | framesInSprite: { vert: 10, horz: 10 }, 23 | }, 24 | { 25 | spriteUrlMask: 26 | 'https://storage.googleapis.com/video-player-media-server-static/thumbnails/high_rez_sprite_%d.jpg', 27 | frameSize: { width: 180, height: 90 }, 28 | framesInSprite: { vert: 5, horz: 5 }, 29 | }, 30 | ], 31 | }; 32 | 33 | export const MODE_OPTIONS = { REGULAR: 'REGULAR', PREVIEW: 'PREVIEW' }; 34 | -------------------------------------------------------------------------------- /src/stories/types.ts: -------------------------------------------------------------------------------- 1 | import { MEDIA_STREAM_TYPES } from '../'; 2 | import { IPlayerConfig } from '../core/config'; 3 | 4 | export type StoryProps = IPlayerConfig & 5 | Partial<{ 6 | videoType: MEDIA_STREAM_TYPES; 7 | color: string; 8 | progressBarMode: string; 9 | }>; 10 | -------------------------------------------------------------------------------- /src/testkit/index.ts: -------------------------------------------------------------------------------- 1 | import { container } from '../core/player-factory'; 2 | import DependencyContainer from '../core/dependency-container'; 3 | 4 | const { asClass } = DependencyContainer; 5 | 6 | function setProperty(target: any, propertyKey: any, propertyValue: any) { 7 | Reflect.defineProperty(target, propertyKey, { 8 | ...Reflect.getOwnPropertyDescriptor( 9 | target.constructor.prototype, 10 | propertyKey, 11 | ), 12 | get: () => propertyValue, 13 | }); 14 | } 15 | 16 | function resetProperty(target: any, propertyKey: any) { 17 | Reflect.deleteProperty(target, propertyKey); 18 | } 19 | 20 | export { setProperty, resetProperty }; 21 | 22 | export default function createPlayerTestkit(config = {}, adapters: any = []) { 23 | const scope = container.createScope(); 24 | 25 | scope.registerValue('config', config); 26 | scope.registerValue('themeConfig', null); 27 | scope.registerValue('availablePlaybackAdapters', [...adapters]); 28 | 29 | return { 30 | getModule(name: string) { 31 | return scope.resolve(name); 32 | }, 33 | registerModule(name: string, fn: Function) { 34 | scope.register(name, asClass(fn)); 35 | }, 36 | registerModuleAsSingleton(name: string, fn: Function) { 37 | scope.register(name, asClass(fn).scoped()); 38 | }, 39 | setConfig(newConfig: object) { 40 | scope.registerValue('config', { 41 | ...newConfig, 42 | }); 43 | }, 44 | setPlaybackAdapters(newAdapters: any) { 45 | scope.registerValue('availablePlaybackAdapters', [...newAdapters]); 46 | }, 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /src/typings/externals.d.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wix-incubator/playable/d84eb15b309d21577f5ac850b3690ec97920b4d5/src/typings/externals.d.ts -------------------------------------------------------------------------------- /src/typings/internals.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.scss'; 2 | declare module '*.svg'; 3 | declare module '*.dot'; 4 | 5 | declare module 'dashjs/build/es5/index_mediaplayerOnly'; 6 | -------------------------------------------------------------------------------- /src/utils/device-detection.ts: -------------------------------------------------------------------------------- 1 | declare const window: { 2 | navigator: any; 3 | MSStream: any; 4 | }; 5 | 6 | const IPHONE_PATTERN = /iphone/i; 7 | const IPOD_PATTERN = /ipod/i; 8 | const IPAD_PATTERN = /ipad/i; 9 | const ANDROID_PATTERN = /(android)/i; 10 | const SAFARI_PATTERN = /^((?!chrome|android).)*safari/i; 11 | const DESKTOP_SAFARI_PATTERN = /^((?!chrome|android|iphone|ipod|ipad).)*safari/i; 12 | 13 | // There is some iPhone/iPad/iPod in Windows Phone... 14 | // https://msdn.microsoft.com/en-us/library/hh869301(v=vs.85).aspx 15 | const isIE = () => !!window.MSStream; 16 | 17 | const getUserAgent = () => window.navigator && window.navigator.userAgent; 18 | 19 | const isIPhone = () => !isIE() && IPHONE_PATTERN.test(getUserAgent()); 20 | 21 | const isIPod = () => !isIE() && IPOD_PATTERN.test(getUserAgent()); 22 | 23 | const isIPad = () => !isIE() && IPAD_PATTERN.test(getUserAgent()); 24 | 25 | const isIOS = () => isIPhone() || isIPod() || isIPad(); 26 | 27 | const isAndroid = () => ANDROID_PATTERN.test(getUserAgent()); 28 | 29 | const isDesktopSafari = () => DESKTOP_SAFARI_PATTERN.test(getUserAgent()); 30 | 31 | const isSafari = () => SAFARI_PATTERN.test(getUserAgent()); 32 | 33 | export { 34 | isIPhone, 35 | isIPod, 36 | isIPad, 37 | isIOS, 38 | isAndroid, 39 | isDesktopSafari, 40 | isSafari, 41 | }; 42 | -------------------------------------------------------------------------------- /src/utils/environment-detection.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * `true` if we are running inside a web browser, `false` otherwise (e.g. running inside Node.js). 3 | */ 4 | export const isBrowser = typeof window !== 'undefined'; 5 | 6 | /** 7 | * This is a map which lists native support of formats and APIs. 8 | * It gets filled during runtime with the relevant values to the current environment. 9 | */ 10 | export const NativeEnvironmentSupport: { [format: string]: boolean } = { 11 | MSE: false, 12 | HLS: false, 13 | DASH: false, 14 | MP4: false, 15 | WEBM: false, 16 | OGG: false, 17 | MOV: false, 18 | MKV: false, 19 | }; 20 | 21 | /* ignore coverage */ 22 | function detectEnvironment() { 23 | if (!isBrowser) { 24 | return; // Not in a browser 25 | } 26 | NativeEnvironmentSupport.MSE = 27 | 'WebKitMediaSource' in window || 'MediaSource' in window; 28 | const video = document.createElement('video'); 29 | if (typeof video.canPlayType !== 'function') { 30 | return; // env doesn't support HTMLMediaElement (e.g PhantomJS) 31 | } 32 | if ( 33 | video.canPlayType('application/x-mpegURL') || 34 | video.canPlayType('application/vnd.apple.mpegURL') 35 | ) { 36 | NativeEnvironmentSupport.HLS = true; 37 | } 38 | if (video.canPlayType('application/dash+xml')) { 39 | NativeEnvironmentSupport.DASH = true; 40 | } 41 | if (video.canPlayType('video/mp4')) { 42 | NativeEnvironmentSupport.MP4 = true; 43 | } 44 | if (video.canPlayType('video/webm')) { 45 | NativeEnvironmentSupport.WEBM = true; 46 | } 47 | if (video.canPlayType('video/ogg')) { 48 | NativeEnvironmentSupport.OGG = true; 49 | } 50 | if (video.canPlayType('video/quicktime')) { 51 | NativeEnvironmentSupport.MOV = true; 52 | } 53 | if (video.canPlayType('video/x-matroska')) { 54 | NativeEnvironmentSupport.MKV = true; 55 | } 56 | } 57 | 58 | detectEnvironment(); // Run once 59 | -------------------------------------------------------------------------------- /src/utils/get-mime-type.ts: -------------------------------------------------------------------------------- 1 | import { MimeToStreamTypeMap } from '../constants'; 2 | 3 | const getExtension = (url: string) => { 4 | if (url.lastIndexOf('.') === -1) { 5 | return null; 6 | } 7 | return url.split('.').pop(); 8 | }; 9 | 10 | const getMimeByType = (type: string | null) => { 11 | if (type === null) { 12 | return null; 13 | } 14 | 15 | const entry = Object.entries(MimeToStreamTypeMap).find( 16 | x => x[1] === type.toUpperCase(), 17 | ); 18 | 19 | return Array.isArray(entry) ? entry[0] : null; 20 | }; 21 | 22 | const getMimeByUrl = (url: string) => getMimeByType(getExtension(url)); 23 | 24 | export { getMimeByType, getMimeByUrl }; 25 | -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | info(message: any, ...optionalParams: any[]) { 3 | window.console.info(message, ...optionalParams); 4 | }, 5 | warn(message: any, ...optionalParams: any[]) { 6 | window.console.warn(message, ...optionalParams); 7 | }, 8 | error(name: string, ...optionalParams: any[]) { 9 | window.console.error(name, ...optionalParams); 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /src/utils/promise.ts: -------------------------------------------------------------------------------- 1 | export const isPromiseAvailable = (function() { 2 | const globalNS: any = (function() { 3 | if (typeof global !== 'undefined') { 4 | return global; 5 | } 6 | if (typeof window !== 'undefined') { 7 | return window; 8 | } 9 | throw new Error('unable to locate global object'); 10 | })(); 11 | //tslint:disable-next-line 12 | return globalNS['Promise'] ? true : false; 13 | })(); 14 | -------------------------------------------------------------------------------- /src/utils/script-injector.ts: -------------------------------------------------------------------------------- 1 | interface IScriptAttributes { 2 | async: boolean; 3 | crossOrigin: string | null; 4 | text: string; 5 | type: string; 6 | } 7 | 8 | const injectScript = (src: string, props?: IScriptAttributes) => { 9 | const scripts = Array.prototype.slice.call( 10 | document.getElementsByTagName('script'), 11 | ); 12 | const links = scripts.map((s: HTMLScriptElement) => s.src); 13 | 14 | if (links.indexOf(src) !== -1) { 15 | return; 16 | } 17 | 18 | const head = document.getElementsByTagName('head')[0]; 19 | const script = document.createElement('script'); 20 | script.src = src; 21 | script.type = 'text/javascript'; 22 | 23 | if (props) { 24 | script.async = props.async; 25 | script.crossOrigin = props.crossOrigin; 26 | script.text = props.text; 27 | script.type = props.type; 28 | } 29 | 30 | head.appendChild(script); 31 | }; 32 | 33 | export default injectScript; 34 | -------------------------------------------------------------------------------- /src/utils/video-data.ts: -------------------------------------------------------------------------------- 1 | export function getTimePercent(time: number, durationTime: number): number { 2 | if (!durationTime) { 3 | return 0; 4 | } 5 | 6 | return parseFloat(((time / durationTime) * 100).toFixed(2)); 7 | } 8 | 9 | export function getOverallBufferedPercent( 10 | buffered: TimeRanges, 11 | currentTime: number = 0, 12 | duration: number = 0, 13 | ) { 14 | if (!buffered || !buffered.length || !duration) { 15 | return 0; 16 | } 17 | 18 | const info = getNearestBufferSegmentInfo(buffered, currentTime); 19 | 20 | return getTimePercent(info.end, duration); 21 | } 22 | 23 | export function getOverallPlayedPercent(currentTime = 0, duration = 0) { 24 | return getTimePercent(currentTime, duration); 25 | } 26 | 27 | export function geOverallBufferLength(buffered: TimeRanges) { 28 | let size = 0; 29 | 30 | if (!buffered || !buffered.length) { 31 | return size; 32 | } 33 | 34 | for (let i = 0; i < buffered.length; i += 1) { 35 | size += buffered.end(i) - buffered.start(i); 36 | } 37 | 38 | return size; 39 | } 40 | 41 | export function getNearestBufferSegmentInfo( 42 | buffered: TimeRanges, 43 | currentTime?: number, 44 | ) { 45 | let i = 0; 46 | 47 | if (!buffered || !buffered.length) { 48 | return null; 49 | } 50 | 51 | while ( 52 | i < buffered.length - 1 && 53 | !(buffered.start(i) <= currentTime && currentTime <= buffered.end(i)) 54 | ) { 55 | i += 1; 56 | } 57 | 58 | return { 59 | start: buffered.start(i), 60 | end: buffered.end(i), 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /src/with-dash.ts: -------------------------------------------------------------------------------- 1 | import Playable from './index'; 2 | import DASHAdapter from './adapters/dash'; 3 | 4 | Playable.registerPlaybackAdapter(DASHAdapter); 5 | 6 | export default Playable; 7 | export * from './index'; 8 | -------------------------------------------------------------------------------- /src/with-hls.ts: -------------------------------------------------------------------------------- 1 | import Playable from './index'; 2 | import HLSAdapter from './adapters/hls'; 3 | 4 | Playable.registerPlaybackAdapter(HLSAdapter); 5 | 6 | export default Playable; 7 | export * from './index'; 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "sourceMap": true, 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "declaration": true, 8 | "noImplicitAny": true, 9 | "noUnusedParameters": true, 10 | "noUnusedLocals": true, 11 | "esModuleInterop": true, 12 | "importHelpers": true, 13 | "skipLibCheck": true, 14 | "target": "ES5", 15 | "jsx": "react", 16 | "lib": [ 17 | "dom", 18 | "es2016", 19 | "es2017" 20 | ], 21 | }, 22 | "include": [ 23 | "./src/**/*.ts" 24 | ], 25 | "exclude": ["./src/stories/**/*.ts"] 26 | } 27 | -------------------------------------------------------------------------------- /tslint-rules/utils/ast.ts: -------------------------------------------------------------------------------- 1 | function isPlayerApiDecorator(decorator) { 2 | return decorator.expression.expression.escapedText === 'playerAPI'; 3 | } 4 | 5 | function getDecoratorExpressionName(decorator) { 6 | return decorator.expression.expression.escapedText; 7 | } 8 | 9 | function getDecoratorArguments(decorator) { 10 | return decorator.expression.arguments; 11 | } 12 | 13 | export { 14 | getDecoratorExpressionName, 15 | isPlayerApiDecorator, 16 | getDecoratorArguments, 17 | }; 18 | -------------------------------------------------------------------------------- /tslint-rules/utils/isPlayerApiDecorator.ts: -------------------------------------------------------------------------------- 1 | import { getDecoratorExpressionName } from './ast'; 2 | 3 | const PLAYER_API_DECORATOR_NAME = 'playerAPI'; 4 | 5 | function isPlayerApiDecorator(decorator) { 6 | return getDecoratorExpressionName(decorator) === PLAYER_API_DECORATOR_NAME; 7 | } 8 | 9 | export { PLAYER_API_DECORATOR_NAME }; 10 | 11 | export default isPlayerApiDecorator; 12 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint-eslint-rules" 4 | ], 5 | "rules": { 6 | "trailing-comma": [true, {"multiline": "always", "singleline": "never"}], 7 | "no-conditional-assignment": true, 8 | "no-console": true, 9 | "no-constant-condition": true, // tslint-eslint-rules 10 | "no-debugger": true, 11 | "no-duplicate-case": true, // tslint-eslint-rules 12 | "no-empty": [true, "allow-empty-catch"], 13 | "no-empty-character-class": true, // tslint-eslint-rules 14 | "no-ex-assign": true, // tslint-eslint-rules 15 | "no-extra-boolean-cast": true, // tslint-eslint-rules 16 | "no-inner-declarations": [true, "both"], // tslint-eslint-rules 17 | "no-invalid-regexp": true, // tslint-eslint-rules 18 | "no-regex-spaces": true, // tslint-eslint-rules 19 | "use-isnan": true, 20 | "valid-typeof": true, // tslint-eslint-rules 21 | "cyclomatic-complexity": true, 22 | "curly": [true, "all"], 23 | "switch-default": true, 24 | "no-string-literal": true, 25 | "triple-equals": [true, "smart"], 26 | "forin": true, 27 | "ban": true, 28 | "no-arg": true, 29 | "no-eval": true, 30 | "no-switch-case-fall-through": true, 31 | "no-invalid-this": true, 32 | "no-sparse-arrays": true, 33 | "label-position": true, 34 | "no-construct": true, 35 | "no-unused-expression": [true, "allow-fast-null-checks"], 36 | "no-duplicate-variable": true, 37 | "no-string-throw": true, 38 | "radix": true, 39 | "no-use-before-declare": true, 40 | "no-var-keyword": true, 41 | "object-literal-shorthand": [true, "always"], 42 | "prefer-const": true, 43 | "jsdoc-format": [true, "check-multiline-start"], 44 | "no-redundant-jsdoc": false, 45 | "no-shadowed-variable": true, 46 | "player-api": true // use `[true, "require-jsdoc"]` to enforce JSDoc before `playerAPI` method 47 | }, 48 | "linterOptions": { 49 | "exclude": [ 50 | "src/e2e/*.ts", 51 | "src/**/*.spec.ts", 52 | "tslint-rules/*" 53 | ] 54 | }, 55 | "rulesDirectory": "tslint-rules" 56 | } 57 | -------------------------------------------------------------------------------- /wallaby.js: -------------------------------------------------------------------------------- 1 | module.exports = wallaby => { 2 | const config = require('haste-preset-playable/src/config/wallabyConfig')( 3 | wallaby, 4 | ); 5 | const { hints } = config; 6 | config.hints = { 7 | hints, 8 | ignoreCoverage: /ignore coverage/, 9 | }; 10 | 11 | return config; 12 | }; 13 | --------------------------------------------------------------------------------