bitmovin-player-ui 3.74.0 → 3.76.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -93,7 +93,9 @@ export declare class SeekBar extends Component<SeekBarConfig> {
93
93
  private label;
94
94
  private seekBarMarkersContainer;
95
95
  private timelineMarkersHandler;
96
+ private uiBoundingRect;
96
97
  private player;
98
+ private uiManager;
97
99
  protected seekBarType: SeekBarType;
98
100
  protected isUiShown: boolean;
99
101
  /**
@@ -206,7 +208,8 @@ export declare class SeekBar extends Component<SeekBarConfig> {
206
208
  */
207
209
  getLabel(): SeekBarLabel | null;
208
210
  protected onSeekEvent(): void;
209
- protected onSeekPreviewEvent(percentage: number, scrubbing: boolean): void;
211
+ private updateLabelPosition;
212
+ protected onSeekPreviewEvent(percentage: number, targetOffsetPx: number, scrubbing: boolean): void;
210
213
  protected onSeekedEvent(percentage: number): void;
211
214
  /**
212
215
  * Gets the event that is fired when a scrubbing seek operation is started.
@@ -92,6 +92,12 @@ var SeekBar = exports.SeekBar = /** @class */ (function (_super) {
92
92
  _this.player.seek(targetPlaybackPosition, 'ui');
93
93
  }
94
94
  };
95
+ _this.updateLabelPosition = function (pixelPosition) {
96
+ if (!_this.uiBoundingRect) {
97
+ _this.uiBoundingRect = _this.uiManager.getUI().getDomElement().get(0).getBoundingClientRect();
98
+ }
99
+ _this.label.setPositionInBounds(pixelPosition, _this.uiBoundingRect);
100
+ };
95
101
  var keyStepIncrements = _this.config.keyStepIncrements || {
96
102
  leftRight: 1,
97
103
  upDown: 5,
@@ -154,6 +160,7 @@ var SeekBar = exports.SeekBar = /** @class */ (function (_super) {
154
160
  if (configureSeek === void 0) { configureSeek = true; }
155
161
  _super.prototype.configure.call(this, player, uimanager);
156
162
  this.player = player;
163
+ this.uiManager = uimanager;
157
164
  // Apply scaling transform to the backdrop bar to have all bars rendered similarly
158
165
  // (the call must be up here to be executed for the volume slider as well)
159
166
  this.setPosition(this.seekBarBackdrop, 100);
@@ -170,9 +177,16 @@ var SeekBar = exports.SeekBar = /** @class */ (function (_super) {
170
177
  }
171
178
  uimanager.onControlsShow.subscribe(function () {
172
179
  _this.isUiShown = true;
180
+ if (!player.isLive() && !_this.smoothPlaybackPositionUpdater.isActive()) {
181
+ playbackPositionHandler(null, true);
182
+ _this.smoothPlaybackPositionUpdater.start();
183
+ }
173
184
  });
174
185
  uimanager.onControlsHide.subscribe(function () {
175
186
  _this.isUiShown = false;
187
+ if (_this.smoothPlaybackPositionUpdater.isActive()) {
188
+ _this.smoothPlaybackPositionUpdater.clear();
189
+ }
176
190
  });
177
191
  var isPlaying = false;
178
192
  var scrubbing = false;
@@ -240,13 +254,12 @@ var SeekBar = exports.SeekBar = /** @class */ (function (_super) {
240
254
  _this.setSeeking(true);
241
255
  scrubbing = false;
242
256
  };
243
- var onPlayerSeeked = function (event, forceUpdate) {
257
+ var onPlayerSeeked = function (event) {
244
258
  if (event === void 0) { event = null; }
245
- if (forceUpdate === void 0) { forceUpdate = false; }
246
259
  isPlayerSeeking = false;
247
260
  _this.setSeeking(false);
248
261
  // update playback position when a seek has finished
249
- playbackPositionHandler(event, forceUpdate);
262
+ playbackPositionHandler(event, true);
250
263
  };
251
264
  var restorePlayingState = function () {
252
265
  // Continue playback after seek if player was playing when seek started
@@ -348,6 +361,7 @@ var SeekBar = exports.SeekBar = /** @class */ (function (_super) {
348
361
  // is positioned absolutely and must therefore be updated when the size of the seekbar changes.
349
362
  player.on(player.exports.PlayerEvent.PlayerResized, function () {
350
363
  _this.refreshPlaybackPosition();
364
+ _this.uiBoundingRect = _this.uiManager.getUI().getDomElement().get(0).getBoundingClientRect();
351
365
  });
352
366
  // Additionally, when this code is called, the seekbar is not part of the UI yet and therefore does not have a size,
353
367
  // resulting in a wrong initial position of the marker. Refreshing it once the UI is configured solved this issue.
@@ -545,10 +559,12 @@ var SeekBar = exports.SeekBar = /** @class */ (function (_super) {
545
559
  if (_this.player.vr != null) {
546
560
  e.stopPropagation();
547
561
  }
548
- var targetPercentage = 100 * _this.getOffset(e);
562
+ var offset = _this.getOffset(e);
563
+ var targetPercentage = 100 * offset;
564
+ var seekPositionPx = offset * _this.seekBar.width();
549
565
  _this.setSeekPosition(targetPercentage);
550
566
  _this.setPlaybackPosition(targetPercentage);
551
- _this.onSeekPreviewEvent(targetPercentage, true);
567
+ _this.onSeekPreviewEvent(targetPercentage, seekPositionPx, true);
552
568
  };
553
569
  var mouseTouchUpHandler = function (e) {
554
570
  var _a;
@@ -593,9 +609,11 @@ var SeekBar = exports.SeekBar = /** @class */ (function (_super) {
593
609
  if (seeking) {
594
610
  mouseTouchMoveHandler(e);
595
611
  }
596
- var position = 100 * _this.getOffset(e);
597
- _this.setSeekPosition(position);
598
- _this.onSeekPreviewEvent(position, false);
612
+ var offset = _this.getOffset(e);
613
+ var seekPositionPercentage = 100 * offset;
614
+ var seekPositionPx = offset * _this.seekBar.width();
615
+ _this.setSeekPosition(seekPositionPercentage);
616
+ _this.onSeekPreviewEvent(seekPositionPercentage, seekPositionPx, false);
599
617
  if (_this.hasLabel() && _this.getLabel().isHidden()) {
600
618
  _this.getLabel().show();
601
619
  }
@@ -809,7 +827,7 @@ var SeekBar = exports.SeekBar = /** @class */ (function (_super) {
809
827
  SeekBar.prototype.onSeekEvent = function () {
810
828
  this.seekBarEvents.onSeek.dispatch(this);
811
829
  };
812
- SeekBar.prototype.onSeekPreviewEvent = function (percentage, scrubbing) {
830
+ SeekBar.prototype.onSeekPreviewEvent = function (percentage, targetOffsetPx, scrubbing) {
813
831
  var snappedMarker = this.timelineMarkersHandler && this.timelineMarkersHandler.getMarkerAtPosition(percentage);
814
832
  var seekPositionPercentage = percentage;
815
833
  if (snappedMarker) {
@@ -831,9 +849,7 @@ var SeekBar = exports.SeekBar = /** @class */ (function (_super) {
831
849
  }
832
850
  }
833
851
  if (this.label) {
834
- this.label.getDomElement().css({
835
- 'left': seekPositionPercentage + '%',
836
- });
852
+ this.updateLabelPosition(targetOffsetPx);
837
853
  }
838
854
  this.seekBarEvents.onSeekPreview.dispatch(this, {
839
855
  scrubbing: scrubbing,
@@ -22,9 +22,12 @@ export declare class SeekBarLabel extends Container<SeekBarLabelConfig> {
22
22
  private appliedMarkerCssClasses;
23
23
  private player;
24
24
  private uiManager;
25
+ private readonly container;
26
+ private readonly caret;
25
27
  constructor(config?: SeekBarLabelConfig);
26
28
  configure(player: PlayerAPI, uimanager: UIInstanceManager): void;
27
29
  private handleSeekPreview;
30
+ setPositionInBounds(seekPositionPx: number, bounds: DOMRect): void;
28
31
  /**
29
32
  * Sets arbitrary text on the label.
30
33
  * @param text the text to show on the label
@@ -78,18 +78,20 @@ var SeekBarLabel = /** @class */ (function (_super) {
78
78
  _this.titleLabel = new label_1.Label({ cssClasses: ['seekbar-label-title'] });
79
79
  _this.thumbnail = new component_1.Component({ cssClasses: ['seekbar-thumbnail'], role: 'img' });
80
80
  _this.thumbnailImageLoader = new imageloader_1.ImageLoader();
81
+ _this.container = new container_1.Container({
82
+ components: [
83
+ _this.thumbnail,
84
+ new container_1.Container({
85
+ components: [_this.titleLabel, _this.timeLabel],
86
+ cssClass: 'seekbar-label-metadata',
87
+ }),
88
+ ],
89
+ cssClass: 'seekbar-label-inner',
90
+ });
91
+ _this.caret = new label_1.Label({ cssClasses: ['seekbar-label-caret'] });
81
92
  _this.config = _this.mergeConfig(config, {
82
93
  cssClass: 'ui-seekbar-label',
83
- components: [new container_1.Container({
84
- components: [
85
- _this.thumbnail,
86
- new container_1.Container({
87
- components: [_this.titleLabel, _this.timeLabel],
88
- cssClass: 'seekbar-label-metadata',
89
- })
90
- ],
91
- cssClass: 'seekbar-label-inner',
92
- })],
94
+ components: [_this.container, _this.caret],
93
95
  hidden: true,
94
96
  }, _this.config);
95
97
  return _this;
@@ -111,6 +113,25 @@ var SeekBarLabel = /** @class */ (function (_super) {
111
113
  uimanager.getConfig().events.onUpdated.subscribe(init);
112
114
  init();
113
115
  };
116
+ SeekBarLabel.prototype.setPositionInBounds = function (seekPositionPx, bounds) {
117
+ this.getDomElement().css('left', seekPositionPx + 'px');
118
+ // Check parent container as it has a padding that needs to be considered
119
+ var labelBounding = this.container.getDomElement().get(0).parentElement.getBoundingClientRect();
120
+ var preventOverflowOffset = 0;
121
+ if (labelBounding.right > bounds.right) {
122
+ preventOverflowOffset = labelBounding.right - bounds.right;
123
+ }
124
+ else if (labelBounding.left < bounds.left) {
125
+ preventOverflowOffset = labelBounding.left - bounds.left;
126
+ }
127
+ if (preventOverflowOffset !== 0) {
128
+ this.getDomElement().css('left', seekPositionPx - preventOverflowOffset + 'px');
129
+ this.caret.getDomElement().css('transform', "translateX(".concat(preventOverflowOffset, "px)"));
130
+ }
131
+ else {
132
+ this.caret.getDomElement().css('transform', null);
133
+ }
134
+ };
114
135
  /**
115
136
  * Sets arbitrary text on the label.
116
137
  * @param text the text to show on the label
@@ -16,7 +16,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.ClickOverlay = exports.VolumeControlButton = exports.TitleBar = exports.SubtitleSelectBox = exports.SubtitleOverlay = exports.SeekBarLabel = exports.RecommendationOverlay = exports.ErrorMessageOverlay = exports.Component = exports.CastToggleButton = exports.CastStatusOverlay = exports.AudioTrackSelectBox = exports.AudioQualitySelectBox = exports.Label = exports.Container = exports.UIContainer = exports.Watermark = exports.VRToggleButton = exports.VolumeToggleButton = exports.VideoQualitySelectBox = exports.ToggleButton = exports.SettingsToggleButton = exports.SettingsPanel = exports.ItemSelectionList = exports.SelectBox = exports.SeekBar = exports.PlaybackToggleButton = exports.PlaybackTimeLabelMode = exports.PlaybackTimeLabel = exports.HugePlaybackToggleButton = exports.FullscreenToggleButton = exports.ControlBar = exports.Button = exports.ListOrientation = exports.ListNavigationGroup = exports.RootNavigationGroup = exports.NavigationGroup = exports.SpatialNavigation = exports.I18n = exports.i18n = exports.ErrorUtils = exports.StorageUtils = exports.BrowserUtils = exports.UIUtils = exports.PlayerUtils = exports.StringUtils = exports.ArrayUtils = exports.DemoFactory = exports.UIFactory = exports.version = void 0;
18
18
  exports.ListSelector = exports.QuickSeekButton = exports.ReplayButton = exports.SettingsPanelItem = exports.SubtitleSettingsPanelPage = exports.SettingsPanelPageOpenButton = exports.SettingsPanelPageBackButton = exports.SettingsPanelPage = exports.AudioTrackListBox = exports.SubtitleListBox = exports.ListBox = exports.SubtitleSettingsResetButton = exports.WindowOpacitySelectBox = exports.WindowColorSelectBox = exports.SubtitleSettingsLabel = exports.SubtitleSettingSelectBox = exports.FontSizeSelectBox = exports.FontOpacitySelectBox = exports.FontFamilySelectBox = exports.FontColorSelectBox = exports.CharacterEdgeSelectBox = exports.BackgroundOpacitySelectBox = exports.BackgroundColorSelectBox = exports.Spacer = exports.PictureInPictureToggleButton = exports.VolumeSlider = exports.AirPlayToggleButton = exports.MetadataLabelContent = exports.MetadataLabel = exports.CloseButton = exports.PlaybackToggleOverlay = exports.CastUIContainer = exports.BufferingOverlay = exports.HugeReplayButton = exports.PlaybackSpeedSelectBox = exports.AdClickOverlay = exports.AdMessageLabel = exports.AdSkipButton = void 0;
19
- exports.version = '3.74.0';
19
+ exports.version = '3.76.0';
20
20
  // Management
21
21
  __exportStar(require("./uimanager"), exports);
22
22
  __exportStar(require("./uiconfig"), exports);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bitmovin-player-ui",
3
- "version": "3.74.0",
3
+ "version": "3.76.0",
4
4
  "description": "Bitmovin Player UI Framework",
5
5
  "main": "./dist/js/framework/main.js",
6
6
  "types": "./dist/js/framework/main.d.ts",
@@ -114,7 +114,7 @@ describe('SeekBar', () => {
114
114
 
115
115
  jest.spyOn(playerMock, 'getSeekableRange').mockImplementation(() => ({start: 26, end: 30}));
116
116
 
117
- seekbar['onSeekPreviewEvent'](40, true)
117
+ seekbar['onSeekPreviewEvent'](40, 100, true);
118
118
 
119
119
  playerMock.eventEmitter.fireSegmentRequestFinished();
120
120
 
@@ -127,19 +127,19 @@ describe('SeekBar', () => {
127
127
  it('will update the scrubber location after a successful segment request download and the user is not scrubbing', () => {
128
128
  jest.spyOn(playerMock, 'getSeekableRange').mockImplementation(() => ({start: 26, end: 30}));
129
129
 
130
- seekbar['onSeekPreviewEvent'](18, false)
130
+ seekbar['onSeekPreviewEvent'](18, 100, false);
131
131
 
132
132
  playerMock.eventEmitter.fireSegmentRequestFinished();
133
133
 
134
134
  expect(setPlaybackPositionSpy).toHaveBeenLastCalledWith(18);
135
- expect(setBufferPositionSpy).toHaveBeenLastCalledWith(18)
135
+ expect(setBufferPositionSpy).toHaveBeenLastCalledWith(18);
136
136
  });
137
137
  });
138
138
  });
139
139
 
140
140
  describe('group playback', () => {
141
141
  beforeEach(() => {
142
- jest.spyOn(playerMock, 'getDuration').mockReturnValue(0)
142
+ jest.spyOn(playerMock, 'getDuration').mockReturnValue(0);
143
143
  seekbar.configure(playerMock, uiInstanceManagerMock);
144
144
  });
145
145
 
@@ -2,6 +2,7 @@ import { MockHelper, TestingPlayerAPI } from '../helper/MockHelper';
2
2
  import { UIInstanceManager } from '../../src/ts/uimanager';
3
3
  import { SeekBarLabel } from '../../src/ts/components/seekbarlabel';
4
4
  import { SeekPreviewEventArgs } from '../../src/ts/components/seekbar';
5
+ import { DOM } from '../../src/ts/dom';
5
6
 
6
7
  let playerMock: TestingPlayerAPI;
7
8
  let uiInstanceManagerMock: UIInstanceManager;
@@ -94,4 +95,97 @@ describe('SeekBarLabel', () => {
94
95
  });
95
96
  });
96
97
  });
98
+
99
+ describe("calculates correct values for thumbnail positioning", () => {
100
+ const uiContainerBoundingRect = {
101
+ x: 200,
102
+ y: 150,
103
+ width: 1600,
104
+ height: 900,
105
+ top: 150,
106
+ right: 1800,
107
+ bottom: 1050,
108
+ left: 200,
109
+ } as DOMRect;
110
+
111
+ let containerGetDomElementMock: () => jest.Mocked<DOM>;
112
+ let caretGetDomElementMock: () => jest.Mocked<DOM>;
113
+
114
+ beforeEach(() => {
115
+ containerGetDomElementMock = jest
116
+ .fn()
117
+ .mockReturnValue(MockHelper.generateDOMMock());
118
+
119
+ containerGetDomElementMock().get = jest.fn().mockReturnValue({
120
+ parentElement: jest.fn().mockReturnValue({
121
+ getBoundingClientRect: jest.fn(),
122
+ }),
123
+ });
124
+
125
+ caretGetDomElementMock = jest.fn().mockReturnValue(MockHelper.generateDOMMock());
126
+
127
+ seekbarLabel["container"].getDomElement = containerGetDomElementMock;
128
+ seekbarLabel["caret"].getDomElement = caretGetDomElementMock;
129
+ });
130
+
131
+ it("when thumbnail within UI container bounds", () => {
132
+ const labelRect = {
133
+ x: 400,
134
+ y: 700,
135
+ width: 200,
136
+ height: 120,
137
+ top: 700,
138
+ right: 600,
139
+ bottom: 820,
140
+ left: 400,
141
+ } as DOMRect;
142
+
143
+ containerGetDomElementMock().get(0).parentElement!.getBoundingClientRect =
144
+ jest.fn().mockReturnValue(labelRect);
145
+
146
+ seekbarLabel.setPositionInBounds(100, uiContainerBoundingRect);
147
+
148
+ expect(caretGetDomElementMock().css).toHaveBeenCalledWith('transform', null);
149
+ });
150
+
151
+ it("when thumbnail would overflow UI container leftside", () => {
152
+ const labelRect = {
153
+ x: 180,
154
+ y: 700,
155
+ width: 200,
156
+ height: 120,
157
+ top: 700,
158
+ right: 380,
159
+ bottom: 820,
160
+ left: 180,
161
+ } as DOMRect;
162
+
163
+ containerGetDomElementMock().get(0).parentElement!.getBoundingClientRect =
164
+ jest.fn().mockReturnValue(labelRect);
165
+
166
+ seekbarLabel.setPositionInBounds(100, uiContainerBoundingRect);
167
+
168
+ expect(seekbarLabel.getDomElement().css).toHaveBeenCalledWith('left', '120px');
169
+ });
170
+
171
+ it("when thumbnail would overflow UI container rightside", () => {
172
+ const labelRect = {
173
+ x: 1650,
174
+ y: 700,
175
+ width: 200,
176
+ height: 120,
177
+ top: 700,
178
+ right: 1850,
179
+ bottom: 820,
180
+ left: 1650,
181
+ } as DOMRect;
182
+
183
+ containerGetDomElementMock().get(0).parentElement!.getBoundingClientRect =
184
+ jest.fn().mockReturnValue(labelRect);
185
+
186
+ seekbarLabel.setPositionInBounds(100, uiContainerBoundingRect);
187
+
188
+ expect(seekbarLabel.getDomElement().css).toHaveBeenCalledWith('left', '50px');
189
+ });
190
+ });
97
191
  });
@@ -21,27 +21,28 @@
21
21
 
22
22
  > .#{$prefix}-container-wrapper {
23
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;
24
41
  }
25
42
 
26
43
  .#{$prefix}-seekbar-label-inner {
27
44
  border-bottom: .2em solid $color-primary;
28
45
 
29
- // bottom arrow from http://www.cssarrowplease.com/
30
- &::after {
31
- border: solid transparent;
32
- border-color: transparent;
33
- border-top-color: $color-primary;
34
- border-width: .5em;
35
- content: ' ';
36
- height: 0;
37
- left: 50%;
38
- margin-left: -.5em;
39
- pointer-events: none;
40
- position: absolute;
41
- top: 100%;
42
- width: 0;
43
- }
44
-
45
46
  > .#{$prefix}-container-wrapper {
46
47
  position: relative;
47
48
 
@@ -114,7 +114,10 @@ export class SeekBar extends Component<SeekBarConfig> {
114
114
  private seekBarMarkersContainer: DOM;
115
115
  private timelineMarkersHandler: TimelineMarkersHandler;
116
116
 
117
+ private uiBoundingRect: DOMRect;
118
+
117
119
  private player: PlayerAPI;
120
+ private uiManager: UIInstanceManager;
118
121
 
119
122
  protected seekBarType: SeekBarType;
120
123
 
@@ -220,6 +223,7 @@ export class SeekBar extends Component<SeekBarConfig> {
220
223
  super.configure(player, uimanager);
221
224
 
222
225
  this.player = player;
226
+ this.uiManager = uimanager;
223
227
 
224
228
  // Apply scaling transform to the backdrop bar to have all bars rendered similarly
225
229
  // (the call must be up here to be executed for the volume slider as well)
@@ -242,10 +246,17 @@ export class SeekBar extends Component<SeekBarConfig> {
242
246
 
243
247
  uimanager.onControlsShow.subscribe(() => {
244
248
  this.isUiShown = true;
249
+ if (!player.isLive() && !this.smoothPlaybackPositionUpdater.isActive()) {
250
+ playbackPositionHandler(null, true);
251
+ this.smoothPlaybackPositionUpdater.start();
252
+ }
245
253
  });
246
254
 
247
255
  uimanager.onControlsHide.subscribe(() => {
248
256
  this.isUiShown = false;
257
+ if (this.smoothPlaybackPositionUpdater.isActive()) {
258
+ this.smoothPlaybackPositionUpdater.clear();
259
+ }
249
260
  });
250
261
 
251
262
  let isPlaying = false;
@@ -323,12 +334,12 @@ export class SeekBar extends Component<SeekBarConfig> {
323
334
  scrubbing = false;
324
335
  };
325
336
 
326
- let onPlayerSeeked = (event: PlayerEventBase = null, forceUpdate: boolean = false ) => {
337
+ let onPlayerSeeked = (event: PlayerEventBase = null) => {
327
338
  isPlayerSeeking = false;
328
339
  this.setSeeking(false);
329
340
 
330
341
  // update playback position when a seek has finished
331
- playbackPositionHandler(event, forceUpdate);
342
+ playbackPositionHandler(event, true);
332
343
  };
333
344
 
334
345
  let restorePlayingState = function () {
@@ -446,6 +457,7 @@ export class SeekBar extends Component<SeekBarConfig> {
446
457
  // is positioned absolutely and must therefore be updated when the size of the seekbar changes.
447
458
  player.on(player.exports.PlayerEvent.PlayerResized, () => {
448
459
  this.refreshPlaybackPosition();
460
+ this.uiBoundingRect = this.uiManager.getUI().getDomElement().get(0).getBoundingClientRect();
449
461
  });
450
462
  // Additionally, when this code is called, the seekbar is not part of the UI yet and therefore does not have a size,
451
463
  // resulting in a wrong initial position of the marker. Refreshing it once the UI is configured solved this issue.
@@ -709,10 +721,13 @@ export class SeekBar extends Component<SeekBarConfig> {
709
721
  e.stopPropagation();
710
722
  }
711
723
 
712
- let targetPercentage = 100 * this.getOffset(e);
724
+ const offset = this.getOffset(e);
725
+ const targetPercentage = 100 * offset;
726
+ const seekPositionPx = offset * this.seekBar.width();
727
+
713
728
  this.setSeekPosition(targetPercentage);
714
729
  this.setPlaybackPosition(targetPercentage);
715
- this.onSeekPreviewEvent(targetPercentage, true);
730
+ this.onSeekPreviewEvent(targetPercentage, seekPositionPx, true);
716
731
  };
717
732
 
718
733
  let mouseTouchUpHandler = (e: MouseEvent | TouchEvent) => {
@@ -770,10 +785,12 @@ export class SeekBar extends Component<SeekBarConfig> {
770
785
  mouseTouchMoveHandler(e);
771
786
  }
772
787
 
773
- let position = 100 * this.getOffset(e);
774
- this.setSeekPosition(position);
788
+ const offset = this.getOffset(e);
789
+ const seekPositionPercentage = 100 * offset;
790
+ const seekPositionPx = offset * this.seekBar.width();
775
791
 
776
- this.onSeekPreviewEvent(position, false);
792
+ this.setSeekPosition(seekPositionPercentage);
793
+ this.onSeekPreviewEvent(seekPositionPercentage, seekPositionPx, false);
777
794
 
778
795
  if (this.hasLabel() && this.getLabel().isHidden()) {
779
796
  this.getLabel().show();
@@ -1013,7 +1030,15 @@ export class SeekBar extends Component<SeekBarConfig> {
1013
1030
  this.seekBarEvents.onSeek.dispatch(this);
1014
1031
  }
1015
1032
 
1016
- protected onSeekPreviewEvent(percentage: number, scrubbing: boolean) {
1033
+ private updateLabelPosition = (pixelPosition: number) => {
1034
+ if (!this.uiBoundingRect) {
1035
+ this.uiBoundingRect = this.uiManager.getUI().getDomElement().get(0).getBoundingClientRect();
1036
+ }
1037
+
1038
+ this.label.setPositionInBounds(pixelPosition, this.uiBoundingRect);
1039
+ };
1040
+
1041
+ protected onSeekPreviewEvent(percentage: number, targetOffsetPx: number, scrubbing: boolean) {
1017
1042
  let snappedMarker = this.timelineMarkersHandler && this.timelineMarkersHandler.getMarkerAtPosition(percentage);
1018
1043
 
1019
1044
  let seekPositionPercentage = percentage;
@@ -1036,9 +1061,7 @@ export class SeekBar extends Component<SeekBarConfig> {
1036
1061
  }
1037
1062
 
1038
1063
  if (this.label) {
1039
- this.label.getDomElement().css({
1040
- 'left': seekPositionPercentage + '%',
1041
- });
1064
+ this.updateLabelPosition(targetOffsetPx);
1042
1065
  }
1043
1066
 
1044
1067
  this.seekBarEvents.onSeekPreview.dispatch(this, {
@@ -36,6 +36,8 @@ export class SeekBarLabel extends Container<SeekBarLabelConfig> {
36
36
  private appliedMarkerCssClasses: string[] = [];
37
37
  private player: PlayerAPI;
38
38
  private uiManager: UIInstanceManager;
39
+ private readonly container: Container<ContainerConfig>;
40
+ private readonly caret: Label<LabelConfig>;
39
41
 
40
42
  constructor(config: SeekBarLabelConfig = {}) {
41
43
  super(config);
@@ -45,17 +47,22 @@ export class SeekBarLabel extends Container<SeekBarLabelConfig> {
45
47
  this.thumbnail = new Component({ cssClasses: ['seekbar-thumbnail'], role: 'img' });
46
48
  this.thumbnailImageLoader = new ImageLoader();
47
49
 
50
+ this.container = new Container({
51
+ components: [
52
+ this.thumbnail,
53
+ new Container({
54
+ components: [this.titleLabel, this.timeLabel],
55
+ cssClass: 'seekbar-label-metadata',
56
+ }),
57
+ ],
58
+ cssClass: 'seekbar-label-inner',
59
+ });
60
+
61
+ this.caret = new Label({ cssClasses: ['seekbar-label-caret'] });
62
+
48
63
  this.config = this.mergeConfig(config, {
49
64
  cssClass: 'ui-seekbar-label',
50
- components: [new Container({
51
- components: [
52
- this.thumbnail,
53
- new Container({
54
- components: [this.titleLabel, this.timeLabel],
55
- cssClass: 'seekbar-label-metadata',
56
- })],
57
- cssClass: 'seekbar-label-inner',
58
- })],
65
+ components: [this.container, this.caret],
59
66
  hidden: true,
60
67
  }, this.config);
61
68
  }
@@ -129,6 +136,28 @@ export class SeekBarLabel extends Container<SeekBarLabelConfig> {
129
136
  }
130
137
  };
131
138
 
139
+ public setPositionInBounds(seekPositionPx: number, bounds: DOMRect) {
140
+ this.getDomElement().css('left', seekPositionPx + 'px');
141
+
142
+ // Check parent container as it has a padding that needs to be considered
143
+ const labelBounding = this.container.getDomElement().get(0).parentElement.getBoundingClientRect();
144
+
145
+ let preventOverflowOffset = 0;
146
+ if (labelBounding.right > bounds.right) {
147
+ preventOverflowOffset = labelBounding.right - bounds.right;
148
+ } else if (labelBounding.left < bounds.left) {
149
+ preventOverflowOffset = labelBounding.left - bounds.left;
150
+ }
151
+
152
+ if (preventOverflowOffset !== 0) {
153
+ this.getDomElement().css('left', seekPositionPx - preventOverflowOffset + 'px');
154
+
155
+ this.caret.getDomElement().css('transform', `translateX(${preventOverflowOffset}px)`);
156
+ } else {
157
+ this.caret.getDomElement().css('transform', null);
158
+ }
159
+ }
160
+
132
161
  /**
133
162
  * Sets arbitrary text on the label.
134
163
  * @param text the text to show on the label