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