/
seekbarlabel.ts
226 lines (188 loc) · 7.72 KB
/
seekbarlabel.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
import {Container, ContainerConfig} from './container';
import {Label, LabelConfig} from './label';
import {Component, ComponentConfig} from './component';
import { UIInstanceManager } from '../uimanager';
import {StringUtils} from '../stringutils';
import {ImageLoader} from '../imageloader';
import {CssProperties} from '../dom';
import { PlayerAPI, Thumbnail } from 'bitmovin-player';
import { SeekBar, SeekPreviewEventArgs } from './seekbar';
import { PlayerUtils } from '../playerutils';
/**
* Configuration interface for a {@link SeekBarLabel}.
*/
export interface SeekBarLabelConfig extends ContainerConfig {
// nothing yet
}
/**
* A label for a {@link SeekBar} that can display the seek target time, a thumbnail, and title (e.g. chapter title).
*/
export class SeekBarLabel extends Container<SeekBarLabelConfig> {
private timeLabel: Label<LabelConfig>;
private titleLabel: Label<LabelConfig>;
private thumbnail: Component<ComponentConfig>;
private thumbnailImageLoader: ImageLoader;
private timeFormat: string;
private appliedMarkerCssClasses: string[] = [];
private player: PlayerAPI;
private uiManager: UIInstanceManager;
constructor(config: SeekBarLabelConfig = {}) {
super(config);
this.timeLabel = new Label({ cssClasses: ['seekbar-label-time'] });
this.titleLabel = new Label({ cssClasses: ['seekbar-label-title'] });
this.thumbnail = new Component({ cssClasses: ['seekbar-thumbnail'], role: 'img' });
this.thumbnailImageLoader = new ImageLoader();
this.config = this.mergeConfig(config, {
cssClass: 'ui-seekbar-label',
components: [new Container({
components: [
this.thumbnail,
new Container({
components: [this.titleLabel, this.timeLabel],
cssClass: 'seekbar-label-metadata',
})],
cssClass: 'seekbar-label-inner',
})],
hidden: true,
}, this.config);
}
configure(player: PlayerAPI, uimanager: UIInstanceManager): void {
super.configure(player, uimanager);
this.player = player;
this.uiManager = uimanager;
uimanager.onSeekPreview.subscribeRateLimited(this.handleSeekPreview, 100);
let init = () => {
// Set time format depending on source duration
this.timeFormat = Math.abs(player.isLive() ? player.getMaxTimeShift() : player.getDuration()) >= 3600 ?
StringUtils.FORMAT_HHMMSS : StringUtils.FORMAT_MMSS;
// Set initial state of title and thumbnail to handle sourceLoaded when switching to a live-stream
this.setTitleText(null);
this.setThumbnail(null);
};
uimanager.getConfig().events.onUpdated.subscribe(init);
init();
}
private handleSeekPreview = (sender: SeekBar, args: SeekPreviewEventArgs) => {
if (this.player.isLive()) {
let maxTimeShift = this.player.getMaxTimeShift();
let timeShiftPreview = maxTimeShift - maxTimeShift * (args.position / 100);
this.setTime(timeShiftPreview);
// In case of a live stream the player expects the time passed into the getThumbnail as a wallClockTime and not
// as a relative timeShift value.
const convertTimeShiftPreviewToWallClockTime = (targetTimeShift: number): number => {
const currentTimeShift = this.player.getTimeShift();
const currentTime = this.player.getCurrentTime();
const wallClockTimeOfLiveEdge = currentTime - currentTimeShift;
return wallClockTimeOfLiveEdge + targetTimeShift;
};
const wallClockTime = convertTimeShiftPreviewToWallClockTime(timeShiftPreview);
this.setThumbnail(this.player.getThumbnail(wallClockTime));
} else {
let time = this.player.getDuration() * (args.position / 100);
this.setTime(time);
const seekableRangeStart = PlayerUtils.getSeekableRangeStart(this.player, 0);
const absoluteSeekTarget = time + seekableRangeStart;
this.setThumbnail(this.player.getThumbnail(absoluteSeekTarget));
}
if (args.marker) {
this.setTitleText(args.marker.marker.title);
} else {
this.setTitleText(null);
}
// Remove CSS classes from previous marker
if (this.appliedMarkerCssClasses.length > 0) {
this.getDomElement().removeClass(this.appliedMarkerCssClasses.join(' '));
this.appliedMarkerCssClasses = [];
}
// Add CSS classes of current marker
if (args.marker) {
const cssClasses = (args.marker.marker.cssClasses || []).map(cssClass => this.prefixCss(cssClass));
this.getDomElement().addClass(cssClasses.join(' '));
this.appliedMarkerCssClasses = cssClasses;
}
};
/**
* Sets arbitrary text on the label.
* @param text the text to show on the label
*/
setText(text: string) {
this.timeLabel.setText(text);
}
/**
* Sets a time to be displayed on the label.
* @param seconds the time in seconds to display on the label
*/
setTime(seconds: number) {
this.setText(StringUtils.secondsToTime(seconds, this.timeFormat));
}
/**
* Sets the text on the title label.
* @param text the text to show on the label
*/
setTitleText(text = '') {
this.titleLabel.setText(text);
}
/**
* Sets or removes a thumbnail on the label.
* @param thumbnail the thumbnail to display on the label or null to remove a displayed thumbnail
*/
setThumbnail(thumbnail: Thumbnail = null) {
let thumbnailElement = this.thumbnail.getDomElement();
if (thumbnail == null) {
thumbnailElement.css({
'background-image': null,
'display': null,
'width': null,
'height': null,
});
}
else {
// We use the thumbnail image loader to make sure the thumbnail is loaded and it's size is known before be can
// calculate the CSS properties and set them on the element.
this.thumbnailImageLoader.load(thumbnail.url, (url, width, height) => {
// can be checked like that because x/y/w/h are either all present or none
// https://www.w3.org/TR/media-frags/#naming-space
if (thumbnail.x !== undefined) {
thumbnailElement.css(this.thumbnailCssSprite(thumbnail, width, height));
} else {
thumbnailElement.css(this.thumbnailCssSingleImage(thumbnail, width, height));
}
});
}
}
private thumbnailCssSprite(thumbnail: Thumbnail, width: number, height: number): CssProperties {
let thumbnailCountX = width / thumbnail.width;
let thumbnailCountY = height / thumbnail.height;
let thumbnailIndexX = thumbnail.x / thumbnail.width;
let thumbnailIndexY = thumbnail.y / thumbnail.height;
let sizeX = 100 * thumbnailCountX;
let sizeY = 100 * thumbnailCountY;
let offsetX = 100 * thumbnailIndexX;
let offsetY = 100 * thumbnailIndexY;
let aspectRatio = 1 / thumbnail.width * thumbnail.height;
// The thumbnail size is set by setting the CSS 'width' and 'padding-bottom' properties. 'padding-bottom' is
// used because it is relative to the width and can be used to set the aspect ratio of the thumbnail.
// A default value for width is set in the stylesheet and can be overwritten from there or anywhere else.
return {
'display': 'inherit',
'background-image': `url(${thumbnail.url})`,
'padding-bottom': `${100 * aspectRatio}%`,
'background-size': `${sizeX}% ${sizeY}%`,
'background-position': `-${offsetX}% -${offsetY}%`,
};
}
private thumbnailCssSingleImage(thumbnail: Thumbnail, width: number, height: number): CssProperties {
let aspectRatio = 1 / width * height;
return {
'display': 'inherit',
'background-image': `url(${thumbnail.url})`,
'padding-bottom': `${100 * aspectRatio}%`,
'background-size': `100% 100%`,
'background-position': `0 0`,
};
}
release(): void {
super.release();
this.uiManager.onSeekPreview.unsubscribe(this.handleSeekPreview);
}
}