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.
- package/CHANGELOG.md +12 -0
- package/artifact/artifact.tar.gz +0 -0
- package/dist/css/bitmovinplayer-ui.css +8 -6
- package/dist/css/bitmovinplayer-ui.min.css +1 -1
- package/dist/js/bitmovinplayer-ui.js +61 -24
- package/dist/js/bitmovinplayer-ui.min.js +1 -1
- package/dist/js/bitmovinplayer-ui.min.js.map +1 -1
- package/dist/js/framework/components/seekbar.d.ts +4 -1
- package/dist/js/framework/components/seekbar.js +28 -12
- package/dist/js/framework/components/seekbarlabel.d.ts +3 -0
- package/dist/js/framework/components/seekbarlabel.js +31 -10
- package/dist/js/framework/main.js +1 -1
- package/package.json +1 -1
- package/spec/components/seekbar.spec.ts +4 -4
- package/spec/components/seekbarlabel.spec.ts +94 -0
- package/src/scss/skin-modern/components/_seekbarlabel.scss +17 -16
- package/src/ts/components/seekbar.ts +34 -11
- package/src/ts/components/seekbarlabel.ts +38 -9
|
@@ -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
|
-
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
597
|
-
|
|
598
|
-
_this.
|
|
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.
|
|
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: [
|
|
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.
|
|
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
|
@@ -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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
774
|
-
|
|
788
|
+
const offset = this.getOffset(e);
|
|
789
|
+
const seekPositionPercentage = 100 * offset;
|
|
790
|
+
const seekPositionPx = offset * this.seekBar.width();
|
|
775
791
|
|
|
776
|
-
this.
|
|
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
|
-
|
|
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.
|
|
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: [
|
|
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
|