@playkit-js/manual-hotspots 3.3.1-canary.0-7e9f2da → 3.3.1-canary.0-2996ab6

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.
@@ -1,72 +0,0 @@
1
- import {RawLayoutHotspot} from './hotspot';
2
- import {HotspotConfigFull, HotspotInputConfig} from './hotspot-config';
3
- import {normalizeHotspotConfig} from './hotspot-normalizer';
4
-
5
- export class HotspotLoader {
6
- constructor(private logger: KalturaPlayerTypes.Logger) {}
7
-
8
- load(configHotspots: HotspotInputConfig[] | undefined): RawLayoutHotspot[] | null {
9
- if (!Array.isArray(configHotspots)) {
10
- this.logger.warn('Hotspots config should be an array');
11
-
12
- return null;
13
- }
14
-
15
- return configHotspots
16
- .map(hotspot => normalizeHotspotConfig(hotspot))
17
- .filter((hotspot, index) => this._validateHotspot(hotspot, index))
18
- .map(hotspot => this._transformConfigToRawHotspot(hotspot));
19
- }
20
-
21
- private _validateHotspot(hotspot: HotspotConfigFull, index: number): boolean {
22
- if (!hotspot.id) {
23
- this.logger.warn(`Hotspot at index ${index} is missing required 'id' field, skipping`);
24
- return false;
25
- }
26
-
27
- if (typeof hotspot.startTime !== 'number') {
28
- this.logger.warn(`Hotspot '${hotspot.id}' is missing required 'startTime' field, skipping`);
29
- return false;
30
- }
31
-
32
- if (!hotspot.layout) {
33
- this.logger.warn(`Hotspot '${hotspot.id}' is missing required 'layout' field, skipping`);
34
- return false;
35
- }
36
-
37
- const requiredLayoutFields = ['relativeX', 'relativeY', 'relativeWidth', 'relativeHeight', 'stageWidth', 'stageHeight'];
38
-
39
- for (const field of requiredLayoutFields) {
40
- if (typeof hotspot.layout[field as keyof typeof hotspot.layout] !== 'number') {
41
- this.logger.warn(`Hotspot '${hotspot.id}' has invalid layout.${field}, skipping`);
42
- return false;
43
- }
44
- }
45
-
46
- if (!hotspot.styles) {
47
- this.logger.warn(`Hotspot '${hotspot.id}' is missing required 'styles' field, skipping`);
48
- return false;
49
- }
50
-
51
- return true;
52
- }
53
-
54
- private _transformConfigToRawHotspot(config: HotspotConfigFull): RawLayoutHotspot {
55
- return {
56
- id: config.id,
57
- startTime: config.startTime,
58
- endTime: config.endTime,
59
- label: config.label,
60
- styles: config.styles as {[key: string]: string},
61
- onClick: config.onClick as RawLayoutHotspot['onClick'],
62
- rawLayout: {
63
- relativeX: config.layout.relativeX,
64
- relativeY: config.layout.relativeY,
65
- relativeWidth: config.layout.relativeWidth,
66
- relativeHeight: config.layout.relativeHeight,
67
- stageWidth: config.layout.stageWidth,
68
- stageHeight: config.layout.stageHeight
69
- }
70
- };
71
- }
72
- }
@@ -1,104 +0,0 @@
1
- import {POSITION_PRESETS, SIZE_PRESETS, DEFAULT_STAGE, DEFAULT_STYLES, POSITION_ANCHORS, PositionPreset, SizePreset} from './hotspot-presets';
2
- import {HotspotConfigFull, HotspotInputConfig} from './hotspot-config';
3
-
4
- let idCounter = 0;
5
- function generateHotspotId(): string {
6
- return `hotspot-${Date.now()}-${idCounter++}`;
7
- }
8
-
9
- function isPositionPreset(value: unknown): value is PositionPreset {
10
- return typeof value === 'string' && value in POSITION_PRESETS;
11
- }
12
-
13
- function isSizePreset(value: unknown): value is SizePreset {
14
- return typeof value === 'string' && value in SIZE_PRESETS;
15
- }
16
-
17
- function calculateAnchoredPosition(
18
- baseX: number,
19
- baseY: number,
20
- width: number,
21
- height: number,
22
- position: PositionPreset
23
- ): {relativeX: number; relativeY: number} {
24
- if (!position || !isPositionPreset(position)) {
25
- return {relativeX: baseX, relativeY: baseY};
26
- }
27
-
28
- const anchor = POSITION_ANCHORS[position];
29
- return {
30
- relativeX: Math.max(0, Math.min(1 - width, baseX - width * anchor.anchorX)),
31
- relativeY: Math.max(0, Math.min(1 - height, baseY - height * anchor.anchorY))
32
- };
33
- }
34
-
35
- export function normalizeHotspotConfig(simplified: HotspotInputConfig): HotspotConfigFull {
36
- const layout = simplified.layout || {};
37
-
38
- const sizePreset: SizePreset = isSizePreset(layout.size) ? layout.size : 'm';
39
- const presetSize = SIZE_PRESETS[sizePreset];
40
- const relativeWidth = layout.relativeWidth ?? presetSize.relativeWidth;
41
- const relativeHeight = layout.relativeHeight ?? presetSize.relativeHeight;
42
-
43
- const positionPreset = isPositionPreset(layout.position) ? layout.position : undefined;
44
-
45
- let baseX: number;
46
- let baseY: number;
47
-
48
- if (layout.relativeX !== undefined) {
49
- baseX = layout.relativeX;
50
- } else if (positionPreset) {
51
- baseX = POSITION_PRESETS[positionPreset].relativeX;
52
- } else {
53
- // default to center
54
- baseX = POSITION_PRESETS['center'].relativeX;
55
- }
56
-
57
- if (layout.relativeY !== undefined) {
58
- baseY = layout.relativeY;
59
- } else if (positionPreset) {
60
- baseY = POSITION_PRESETS[positionPreset].relativeY;
61
- } else {
62
- // default to center
63
- baseY = POSITION_PRESETS['center'].relativeY;
64
- }
65
-
66
- const hasExplicitPosition = layout.relativeX !== undefined || layout.relativeY !== undefined;
67
- const {relativeX, relativeY} = hasExplicitPosition
68
- ? {relativeX: baseX, relativeY: baseY}
69
- : calculateAnchoredPosition(baseX, baseY, relativeWidth, relativeHeight, positionPreset || 'center');
70
-
71
- const userStyles = simplified.styles || {};
72
- const mergedStyles: Record<string, string> = {};
73
-
74
- // defaults
75
- for (const [key, value] of Object.entries(DEFAULT_STYLES)) {
76
- mergedStyles[key] = value;
77
- }
78
-
79
- // user overrides
80
- for (const [key, value] of Object.entries(userStyles)) {
81
- if (value !== undefined) {
82
- mergedStyles[key] = value;
83
- }
84
- }
85
-
86
- const fullConfig: HotspotConfigFull = {
87
- id: simplified.id || generateHotspotId(),
88
- startTime: simplified.startTime,
89
- endTime: simplified.endTime,
90
- label: simplified.label,
91
- onClick: simplified.onClick,
92
- layout: {
93
- relativeX,
94
- relativeY,
95
- relativeWidth,
96
- relativeHeight,
97
- stageWidth: layout.stageWidth ?? DEFAULT_STAGE.stageWidth,
98
- stageHeight: layout.stageHeight ?? DEFAULT_STAGE.stageHeight
99
- },
100
- styles: mergedStyles
101
- };
102
-
103
- return fullConfig;
104
- }
@@ -1,51 +0,0 @@
1
- export type PositionPreset = 'top-left' | 'top' | 'top-right' | 'left' | 'center' | 'right' | 'bottom-left' | 'bottom' | 'bottom-right';
2
-
3
- export type SizePreset = 'xs' | 's' | 'm' | 'l' | 'xl';
4
-
5
- // include 5% padding from edges
6
- export const POSITION_PRESETS: Record<PositionPreset, {relativeX: number; relativeY: number}> = {
7
- 'top-left': {relativeX: 0.05, relativeY: 0.05},
8
- top: {relativeX: 0.5, relativeY: 0.05},
9
- 'top-right': {relativeX: 0.95, relativeY: 0.05},
10
- left: {relativeX: 0.05, relativeY: 0.5},
11
- center: {relativeX: 0.5, relativeY: 0.5},
12
- right: {relativeX: 0.95, relativeY: 0.5},
13
- 'bottom-left': {relativeX: 0.05, relativeY: 0.95},
14
- bottom: {relativeX: 0.5, relativeY: 0.95},
15
- 'bottom-right': {relativeX: 0.95, relativeY: 0.95}
16
- };
17
-
18
- export const SIZE_PRESETS: Record<SizePreset, {relativeWidth: number; relativeHeight: number}> = {
19
- xs: {relativeWidth: 0.12, relativeHeight: 0.08},
20
- s: {relativeWidth: 0.18, relativeHeight: 0.1},
21
- m: {relativeWidth: 0.25, relativeHeight: 0.12},
22
- l: {relativeWidth: 0.32, relativeHeight: 0.14},
23
- xl: {relativeWidth: 0.42, relativeHeight: 0.16}
24
- };
25
-
26
- // used for scaling font sizes and border radius
27
- export const DEFAULT_STAGE = {
28
- stageWidth: 960,
29
- stageHeight: 640
30
- };
31
-
32
- export const DEFAULT_STYLES: Record<string, string> = {
33
- background: 'rgba(0, 0, 0, 0.75)',
34
- color: '#ffffff',
35
- 'border-radius': '12px',
36
- 'font-size': '20px',
37
- 'font-weight': '500',
38
- 'font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif'
39
- };
40
-
41
- export const POSITION_ANCHORS: Record<PositionPreset, {anchorX: number; anchorY: number}> = {
42
- 'top-left': {anchorX: 0, anchorY: 0},
43
- top: {anchorX: 0.5, anchorY: 0},
44
- 'top-right': {anchorX: 1, anchorY: 0},
45
- left: {anchorX: 0, anchorY: 0.5},
46
- center: {anchorX: 0.5, anchorY: 0.5},
47
- right: {anchorX: 1, anchorY: 0.5},
48
- 'bottom-left': {anchorX: 0, anchorY: 1},
49
- bottom: {anchorX: 0.5, anchorY: 1},
50
- 'bottom-right': {anchorX: 1, anchorY: 1}
51
- };
@@ -1,78 +0,0 @@
1
- interface TimelineHotspot {
2
- id: string;
3
- startTime: number; // ms
4
- endTime?: number; // ms (exclusive); undefined => infinite
5
- }
6
-
7
- export class HotspotTimelineSimple<T extends TimelineHotspot> {
8
- private _hotspotsMap: Map<string, T> = new Map();
9
- private _visibleIds: Set<string> = new Set();
10
-
11
- initialize(hotspots: T[]): void {
12
- this._hotspotsMap.clear();
13
- this._visibleIds.clear();
14
-
15
- for (const hotspot of hotspots) {
16
- this._hotspotsMap.set(hotspot.id, hotspot);
17
- }
18
- }
19
-
20
- // Recompute visible set at time currentTimeMs.
21
- // Returns true if visible set changed.
22
- update(currentTimeMs: number): boolean {
23
- const nextVisible = new Set<string>();
24
-
25
- this._hotspotsMap.forEach((h, id) => {
26
- if (currentTimeMs < h.startTime) {
27
- return;
28
- }
29
-
30
- if (h.endTime !== undefined && currentTimeMs >= h.endTime) {
31
- return;
32
- }
33
-
34
- nextVisible.add(id);
35
- });
36
-
37
- const prevVisible = this._visibleIds;
38
-
39
- if (nextVisible.size !== prevVisible.size) {
40
- this._visibleIds = nextVisible;
41
- return true;
42
- }
43
-
44
- let changed = false;
45
- nextVisible.forEach(id => {
46
- if (!changed && !prevVisible.has(id)) {
47
- changed = true;
48
- }
49
- });
50
-
51
- this._visibleIds = nextVisible;
52
-
53
- return changed;
54
- }
55
-
56
- getVisibleHotspots(): T[] {
57
- const result: T[] = [];
58
-
59
- this._visibleIds.forEach(id => {
60
- const hotspot = this._hotspotsMap.get(id);
61
-
62
- if (hotspot) {
63
- result.push(hotspot);
64
- }
65
- });
66
-
67
- return result;
68
- }
69
-
70
- get size(): number {
71
- return this._hotspotsMap.size;
72
- }
73
-
74
- clear(): void {
75
- this._hotspotsMap.clear();
76
- this._visibleIds.clear();
77
- }
78
- }
@@ -1,88 +0,0 @@
1
- export interface Layout {
2
- x: number;
3
- y: number;
4
- width: number;
5
- height: number;
6
- }
7
-
8
- export interface Style {
9
- radiusBorder: number;
10
- fontSize: number;
11
- }
12
-
13
- export interface RawFloatingCuepoint {
14
- id: string;
15
- startTime: number;
16
- endTime?: number;
17
- rawLayout: {
18
- relativeX: number;
19
- relativeY: number;
20
- relativeWidth: number;
21
- relativeHeight: number;
22
- stageWidth: number;
23
- stageHeight: number;
24
- };
25
- }
26
-
27
- export interface FloatingCuepoint extends RawFloatingCuepoint {
28
- layout: Layout;
29
- relativeStyle : Style;
30
- }
31
-
32
- export interface OpenUrl {
33
- type: 'openUrl';
34
- url: string;
35
- }
36
- export interface OpenUrlInNewTab {
37
- type: 'openUrlInNewTab';
38
- url: string;
39
- }
40
- export interface JumpToTime {
41
- type: 'jumpToTime';
42
- jumpToTime: number;
43
- }
44
- interface ScrollToAnchor {
45
- type: 'scrollToAnchor';
46
- selector: string;
47
- scrollBehavior?: ScrollBehavior;
48
- offsetY?: number;
49
- }
50
- interface PostMessage {
51
- type: 'postMessage';
52
- message: unknown;
53
- targetOrigin?: string;
54
- pauseVideo?: boolean;
55
- }
56
-
57
- export type OnClickAction = OpenUrl | OpenUrlInNewTab | JumpToTime | ScrollToAnchor | PostMessage;
58
-
59
- export type RawLayoutHotspot = RawFloatingCuepoint & {
60
- onClick?: OnClickAction;
61
- label?: string;
62
- styles: {[key: string]: string};
63
- };
64
-
65
- export type LayoutHotspot = RawLayoutHotspot & FloatingCuepoint;
66
-
67
- export interface Size {
68
- width: number;
69
- height: number;
70
- }
71
-
72
- export interface Canvas {
73
- playerSize: Size;
74
- videoSize: Size;
75
- }
76
-
77
- export const shallowCompareHotspots = (arrA: LayoutHotspot[], arrB: LayoutHotspot[]) => {
78
- const len = arrA.length;
79
- if (arrB.length !== len) {
80
- return false;
81
- }
82
- for (let i = 0; i < len; i++) {
83
- if (arrA[i] !== arrB[i]) {
84
- return false;
85
- }
86
- }
87
- return true;
88
- };
@@ -1,57 +0,0 @@
1
- export interface ScaleCalculation {
2
- width: number;
3
- height: number;
4
- left: number;
5
- top: number;
6
- scaleToTargetWidth: boolean;
7
- }
8
-
9
- export function scaleVideo(
10
- videoWidth: number,
11
- videoHeight: number,
12
- playerWidth: number,
13
- playerHeight: number,
14
- fLetterBox: boolean
15
- ): ScaleCalculation {
16
- const result: ScaleCalculation = {
17
- width: 0,
18
- height: 0,
19
- left: 0,
20
- top: 0,
21
- scaleToTargetWidth: true
22
- };
23
-
24
- if (videoWidth <= 0 || videoHeight <= 0 || playerWidth <= 0 || playerHeight <= 0) {
25
- return result;
26
- }
27
-
28
- // scale to the target width
29
- const scaleX1 = playerWidth;
30
- const scaleY1 = (videoHeight * playerWidth) / videoWidth;
31
-
32
- // scale to the target height
33
- const scaleX2 = (videoWidth * playerHeight) / videoHeight;
34
- const scaleY2 = playerHeight;
35
-
36
- // now figure out which one we should use
37
- let fScaleOnWidth = scaleX2 > playerWidth;
38
- if (fScaleOnWidth) {
39
- fScaleOnWidth = fLetterBox;
40
- } else {
41
- fScaleOnWidth = !fLetterBox;
42
- }
43
-
44
- if (fScaleOnWidth) {
45
- result.width = Math.abs(scaleX1);
46
- result.height = Math.abs(scaleY1);
47
- result.scaleToTargetWidth = true;
48
- } else {
49
- result.width = Math.abs(scaleX2);
50
- result.height = Math.abs(scaleY2);
51
- result.scaleToTargetWidth = false;
52
- }
53
- result.left = Math.abs((playerWidth - result.width) / 2);
54
- result.top = Math.abs((playerHeight - result.height) / 2);
55
-
56
- return result;
57
- }