UNPKG

bitmovin-player-ui

Version:
262 lines (219 loc) 9.24 kB
import { Container, ContainerConfig } from '../Container'; import { Label, LabelConfig } from '../labels/Label'; import { Component, ComponentConfig } from '../Component'; import { UIInstanceManager } from '../../UIManager'; import { StringUtils } from '../../utils/StringUtils'; import { ImageLoader } from '../../utils/ImageLoader'; import { CssProperties } from '../../DOM'; import { PlayerAPI, Thumbnail } from 'bitmovin-player'; import { SeekBar, SeekPreviewEventArgs } from './SeekBar'; import { PlayerUtils } from '../../utils/PlayerUtils'; /** * Configuration interface for a {@link SeekBarLabel}. * * @category Configs */ 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). * * @category Components */ 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; private readonly container: Container<ContainerConfig>; constructor(config: SeekBarLabelConfig = {}) { super(config); this.timeLabel = new Label({ cssClasses: ['seekbar-label-metadata', 'seekbar-label-time'] }); this.titleLabel = new Label({ cssClasses: ['seekbar-label-metadata', 'seekbar-label-title'] }); this.thumbnail = new Component({ cssClasses: ['seekbar-thumbnail'], role: 'img' }); this.thumbnailImageLoader = new ImageLoader(); this.container = new Container({ components: [this.titleLabel, this.thumbnail, this.timeLabel], cssClass: 'seekbar-label-inner', }); this.config = this.mergeConfig( config, { cssClass: 'ui-seekbar-label', components: [this.container], hidden: true, }, this.config, ); } private initializeTimeFormat = () => { // Set time format depending on source duration const player = this.player; if (player == null) { return; } 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); }; configure(player: PlayerAPI, uimanager: UIInstanceManager): void { super.configure(player, uimanager); this.player = player; this.uiManager = uimanager; uimanager.onSeekPreview.subscribeRateLimited(this.handleSeekPreview, 100); uimanager.getConfig().events.onUpdated.subscribe(this.initializeTimeFormat); player.on(this.player.exports.PlayerEvent.DurationChanged, this.initializeTimeFormat); this.initializeTimeFormat(); } private handleSeekPreview = (sender: SeekBar, args: SeekPreviewEventArgs) => { if (this.player.isLive()) { const maxTimeShift = this.player.getMaxTimeShift(); const 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 { const 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; } }; public setPositionInBounds(seekPositionPx: number, bounds: DOMRect) { this.getDomElement().css('left', seekPositionPx + 'px'); // Check parent container as it has a padding that needs to be considered const labelBounding = this.container.getDomElement().get(0).parentElement.getBoundingClientRect(); let preventOverflowOffset = 0; if (labelBounding.right > bounds.right) { preventOverflowOffset = labelBounding.right - bounds.right; } else if (labelBounding.left < bounds.left) { preventOverflowOffset = labelBounding.left - bounds.left; } if (preventOverflowOffset !== 0) { this.getDomElement().css('left', seekPositionPx - preventOverflowOffset + 'px'); } } /** * 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?: string) { this.titleLabel.setText(text); if (text == null) { this.titleLabel.hide(); } else { this.titleLabel.show(); } } /** * 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) { const thumbnailElement = this.thumbnail.getDomElement(); if (thumbnail == null) { thumbnailElement.css({ 'background-image': null, display: 'none', 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 { const thumbnailCountX = width / thumbnail.width; const thumbnailCountY = height / thumbnail.height; const thumbnailIndexX = thumbnail.x / thumbnail.width; const thumbnailIndexY = thumbnail.y / thumbnail.height; const sizeX = 100 * thumbnailCountX; const sizeY = 100 * thumbnailCountY; const offsetX = 100 * thumbnailIndexX; const offsetY = 100 * thumbnailIndexY; const 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 { const 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); this.player.off(this.player.exports.PlayerEvent.DurationChanged, this.initializeTimeFormat); } }