@24i/bigscreen-sdk 1.0.8-alpha.2178 → 1.0.8-alpha.2180

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@24i/bigscreen-sdk",
3
- "version": "1.0.8-alpha.2178",
3
+ "version": "1.0.8-alpha.2180",
4
4
  "description": "SmartApps BIGscreen SDK monorepo",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -1,59 +1,143 @@
1
1
  import { Storage } from '@24i/bigscreen-sdk/storage';
2
+ import { device } from '@24i/bigscreen-sdk/device';
2
3
  import { debounce } from '@24i/bigscreen-sdk/utils/debounce';
4
+ import { createTimeout } from '@24i/bigscreen-sdk/utils/timers';
5
+ import { MINUTE_IN_MS } from '@24i/bigscreen-sdk/utils/timeConstants';
3
6
  import { generateUuid } from '@24i/bigscreen-sdk/utils/generateUuid';
4
- import type { AnalyticsClient, AnalyticsEventsMap } from '../../interface';
5
- import { mapPayload } from './mapPayload';
6
- import { TRIGGER_NAME_TO_GA_EVENTS } from './constants';
7
- import { GAEventsNames } from './interface';
7
+ import type { AnalyticsClient, AnalyticsEventsMap, SceneInfo } from '../../interface';
8
+ import { mapEventParamsToUrlParams, mapPayload } from './mapPayload';
9
+ import { prepareBody } from './prepareBody';
10
+ import { getUrl } from './getUrl';
11
+ import { generateSessionId } from './generateSessionId';
12
+ import { GOOGLE_ANALYTICS_TRIGGERS, TRIGGER_NAME_TO_GA_EVENTS } from './constants';
13
+ import {
14
+ GAEventsNames,
15
+ QueuedEvent,
16
+ ConstantUrlParams,
17
+ SessionInfo,
18
+ EventUrlParams,
19
+ } from './interface';
8
20
 
9
21
  const SEND_DEBOUNCE_MS = 5000;
10
22
  const MAX_QUEUE_LENGTH = 10;
23
+ // eslint-disable-next-line no-magic-numbers
24
+ const SESSION_TIMEOUT = 30 * MINUTE_IN_MS;
11
25
 
12
26
  const GA_CLIENT_ID = 'GA_client_id';
13
-
14
- const getGAUrl = (id: string, secret: string) => (
15
- // You can use the following URL for debugging if the events are not getting collected.
16
- // `https://www.google-analytics.com/debug/mp/collect?measurement_id=${id}&api_secret=${secret}`
17
- `https://www.google-analytics.com/mp/collect?measurement_id=${id}&api_secret=${secret}`
18
- );
27
+ const GA_SESSION_COUNTER = 'GA_session_counter';
28
+ const GA_FIRST_VISIT = 'GA_first_visit';
19
29
 
20
30
  export class GoogleAnalytics implements AnalyticsClient {
21
31
  name = 'GoogleAnalytics';
22
32
 
23
- clientId = '';
24
-
25
- sessionId = generateUuid();
33
+ constantUrlParams: ConstantUrlParams = {
34
+ cid: '',
35
+ sid: 0,
36
+ uamb: 0,
37
+ uap: '',
38
+ uapv: '',
39
+ uam: '',
40
+ seg: 0,
41
+ sct: 0,
42
+ _p: generateUuid(),
43
+ };
26
44
 
27
45
  a24iOperational = false;
28
46
 
29
- eventsQueue: { name: GAEventsNames, params: Record<string, any> }[] = [];
47
+ lastOnEvent: number | undefined = undefined;
48
+
49
+ eventsQueue: QueuedEvent[] = [];
50
+
51
+ sessionTimeout = createTimeout();
52
+
53
+ isSessionActive = false;
30
54
 
31
- constructor(private id: string, private secret: string) {
55
+ sessionStartSent = false;
56
+
57
+ firstEvent = true;
58
+
59
+ firstVisit: boolean | undefined = undefined;
60
+
61
+ constructor(private id: string) {
32
62
  this.init();
33
63
  }
34
64
 
35
65
  async init() {
36
- this.clientId = await Storage.getItem(GA_CLIENT_ID) ?? '';
37
- if (!this.clientId) {
38
- this.clientId = generateUuid();
39
- await Storage.setItem(GA_CLIENT_ID, this.clientId);
66
+ const params = this.constantUrlParams;
67
+ params.cid = await Storage.getItem(GA_CLIENT_ID) ?? '';
68
+ if (!params.cid) {
69
+ params.cid = generateUuid();
70
+ await Storage.setItem(GA_CLIENT_ID, params.cid);
40
71
  }
72
+ this.firstVisit = !(await Storage.getItem(GA_FIRST_VISIT)) ?? true;
73
+ if (this.firstVisit) {
74
+ await Storage.setItem(GA_FIRST_VISIT, '0');
75
+ }
76
+ await this.startSession();
77
+ params.uap = await device.getPlatformName();
78
+ params.uapv = await device.getPlatformVersion();
79
+ params.uam = await device.getModelName();
80
+ }
81
+
82
+ async startSession() {
83
+ const sessionNumber = parseInt(await Storage.getItem(GA_SESSION_COUNTER) ?? '0') + 1;
84
+ this.constantUrlParams.sct = sessionNumber;
85
+ this.constantUrlParams.sid = generateSessionId();
86
+ await Storage.setItem(GA_SESSION_COUNTER, sessionNumber.toString());
87
+ this.isSessionActive = true;
88
+ this.setSessionTimeout();
89
+ }
90
+
91
+ setSessionTimeout() {
92
+ this.sessionTimeout.set(() => {
93
+ this.isSessionActive = false;
94
+ this.sessionStartSent = false;
95
+ this.constantUrlParams.seg = 0;
96
+ }, SESSION_TIMEOUT);
41
97
  }
42
98
 
43
- onEvent<Event extends keyof AnalyticsEventsMap>(
99
+ // @ts-ignore - cross event typing
100
+ async onEvent<Event extends keyof AnalyticsEventsMap>(
44
101
  triggerName: Event,
45
- payload: Parameters<AnalyticsEventsMap[Event]>,
46
- ): void {
47
- const events = TRIGGER_NAME_TO_GA_EVENTS[triggerName];
48
- events.forEach(async (event) => {
102
+ payload: Parameters<AnalyticsEventsMap[Event]>[0],
103
+ ) {
104
+ if (this.isSessionActive) {
105
+ this.setSessionTimeout();
106
+ } else {
107
+ await this.startSession();
108
+ }
109
+ const events = TRIGGER_NAME_TO_GA_EVENTS[
110
+ triggerName as typeof GOOGLE_ANALYTICS_TRIGGERS[number]
111
+ ];
112
+ const now = Date.now();
113
+ const engagementTime = this.lastOnEvent ? now - this.lastOnEvent : undefined;
114
+ this.lastOnEvent = now;
115
+ await Promise.all(events.map(async (event, index) => {
49
116
  if (event === 'a24i_operational' && !this.shouldContinueWithA24iOperational()) return;
50
- const gtagPayload = mapPayload(event, triggerName, this.sessionId, payload);
51
- this.send(event, gtagPayload);
52
- });
117
+ const gtagPayload = mapPayload(
118
+ event, triggerName, await this.getSessionInfo(
119
+ index === 0 ? engagementTime : undefined,
120
+ ),
121
+ );
122
+ const urlParams = mapEventParamsToUrlParams(payload as SceneInfo);
123
+ this.send(event, gtagPayload, urlParams);
124
+ }));
53
125
  }
54
126
 
55
- private send(name: GAEventsNames, params: Record<string, any>) {
56
- this.eventsQueue.push({ name, params });
127
+ private send(name: GAEventsNames, params: Record<string, any>, urlParams: EventUrlParams) {
128
+ const lastUrlParams = JSON.stringify(
129
+ this.eventsQueue[this.eventsQueue.length - 1]?.urlParams ?? {},
130
+ );
131
+ if (lastUrlParams !== '{}' && lastUrlParams !== JSON.stringify(urlParams)) {
132
+ this.sendQueue();
133
+ }
134
+ if (this.constantUrlParams.seg === 0) {
135
+ this.eventsQueue.push({ name, params, urlParams });
136
+ this.sendQueue();
137
+ this.constantUrlParams.seg = 1;
138
+ return;
139
+ }
140
+ this.eventsQueue.push({ name, params, urlParams });
57
141
  if (this.eventsQueue.length >= MAX_QUEUE_LENGTH) {
58
142
  this.sendQueue();
59
143
  } else {
@@ -62,13 +146,15 @@ export class GoogleAnalytics implements AnalyticsClient {
62
146
  }
63
147
 
64
148
  private sendQueue = () => {
65
- const body = JSON.stringify({
66
- client_id: this.clientId,
67
- events: this.eventsQueue,
68
- });
149
+ if (this.eventsQueue.length === 0) return;
150
+ const body = prepareBody(this.eventsQueue);
151
+ const { urlParams } = this.eventsQueue[0];
69
152
  this.eventsQueue.length = 0;
70
153
  try {
71
- fetch(getGAUrl(this.id, this.secret), { method: 'POST', body });
154
+ fetch(getUrl(this.id, {
155
+ ...this.constantUrlParams,
156
+ ...urlParams,
157
+ }), { method: 'POST', body });
72
158
  } catch (e) {
73
159
  // Effort to send was made.
74
160
  }
@@ -81,4 +167,19 @@ export class GoogleAnalytics implements AnalyticsClient {
81
167
  if (!a24iOperational) this.a24iOperational = true;
82
168
  return !a24iOperational;
83
169
  }
170
+
171
+ private async getSessionInfo(engagementTime: number | undefined): Promise<SessionInfo> {
172
+ const sessionStarted = !this.sessionStartSent;
173
+ this.sessionStartSent = true;
174
+ const { firstEvent } = this;
175
+ this.firstEvent = false;
176
+ const { firstVisit } = this;
177
+ this.firstVisit = false;
178
+ return {
179
+ sessionStarted,
180
+ engagementTime,
181
+ firstEvent,
182
+ firstVisit: firstVisit as boolean,
183
+ };
184
+ }
84
185
  }
@@ -1,17 +1,30 @@
1
- import { AnalyticsEventsNames } from '../../interface';
2
1
  import { GAEventsNames } from './interface';
3
2
 
4
- export const TRIGGER_NAME_TO_GA_EVENTS: Record<AnalyticsEventsNames, GAEventsNames[]> = {
3
+ export const GOOGLE_ANALYTICS_TRIGGERS = [
4
+ 'app_close',
5
+ 'playback_10',
6
+ 'playback_25',
7
+ 'playback_50',
8
+ 'playback_75',
9
+ 'playback_90',
10
+ 'playback_pause',
11
+ 'playback_start',
12
+ 'playback_stop',
13
+ 'player_close',
14
+ 'player_open',
15
+ 'screen_view',
16
+ 'scroll_25',
17
+ 'scroll_50',
18
+ 'scroll_75',
19
+ 'scroll_90',
20
+ ] as const;
21
+
22
+ export const TRIGGER_NAME_TO_GA_EVENTS: Record<
23
+ typeof GOOGLE_ANALYTICS_TRIGGERS[number], GAEventsNames[]
24
+ > = {
5
25
  // App
6
26
  'app_close': ['app_close', 'video_progress'],
7
27
 
8
- // Data loading / Player buffering
9
- 'buffering_start': ['buffering_start'],
10
- 'buffering_stop': ['buffering_start'],
11
-
12
- // Heartbeat
13
- 'heartbeat_5': ['a24i_content'],
14
-
15
28
  // Playback
16
29
  'playback_10': ['video_progress'],
17
30
  'playback_25': ['video_progress'],
@@ -0,0 +1,8 @@
1
+ /* eslint-disable no-magic-numbers */
2
+ /**
3
+ * Recommended way to generate session id by Google.
4
+ * Blackbox
5
+ */
6
+ export const generateSessionId = () => (
7
+ Math.floor(Math.random() * (2147483647 - 0 + 1) + 0)
8
+ );
@@ -0,0 +1,17 @@
1
+ import { ConstantUrlParams, EventUrlParams } from './interface';
2
+
3
+ const TRANSPORT_URL = 'https://www.google-analytics.com/g/collect?';
4
+ // You can use the following URL for debugging if the events are not getting collected.
5
+ // const TRANSPORT_URL = 'https://www.google-analytics.com/debug/g/collec?';
6
+
7
+ export const getUrl = (id: string, params: ConstantUrlParams & EventUrlParams) => {
8
+ const urlParts = [
9
+ TRANSPORT_URL,
10
+ `v=2&tid=${id}`,
11
+ ];
12
+ (Object.keys(params) as (keyof (ConstantUrlParams & EventUrlParams))[])
13
+ .forEach((paramName: keyof (ConstantUrlParams & EventUrlParams)) => {
14
+ urlParts.push(`&${paramName}=${encodeURIComponent(params[paramName])}`);
15
+ });
16
+ return urlParts.join('');
17
+ };
@@ -1 +1,2 @@
1
1
  export { GoogleAnalytics } from './GoogleAnalytics';
2
+ export { GOOGLE_ANALYTICS_TRIGGERS } from './constants';
@@ -8,3 +8,50 @@ export type GAEventsNames = (
8
8
  | 'video_progress'
9
9
  | 'video_start'
10
10
  );
11
+
12
+ export type QueuedEvent = {
13
+ name: string,
14
+ params: Record<string, any>,
15
+ urlParams: EventUrlParams,
16
+ };
17
+
18
+ export type SessionInfo = {
19
+ engagementTime: number | undefined,
20
+ sessionStarted: boolean,
21
+ firstEvent: boolean,
22
+ firstVisit: boolean,
23
+ };
24
+
25
+ export type ConstantUrlParams = {
26
+ /** Client ID (UUIv4 string) */
27
+ cid: string,
28
+ /** Session ID (random number) */
29
+ sid: number,
30
+ /** 1 for mobile devices, 0 otherwise */
31
+ uamb: number,
32
+ /** Platform name */
33
+ uap: string,
34
+ /** Platform version */
35
+ uapv: string,
36
+ /** Device model name */
37
+ uam: string,
38
+ /** Session engagement 0 for first event of session, 1 otherwise */
39
+ seg: number,
40
+ /** Session count */
41
+ sct: number,
42
+ /** Random page hash */
43
+ _p: string,
44
+ };
45
+
46
+ export type EventUrlParams = {
47
+ /** Language iso639-1 */
48
+ ul: string,
49
+ /** Device resolution as WIDTHxHEIGHT */
50
+ sr: string,
51
+ /** Page hash route */
52
+ dl: string,
53
+ /** Page title */
54
+ dt: string,
55
+ /** Page referrer hash route */
56
+ dr: string,
57
+ };
@@ -1,76 +1,29 @@
1
- import type {
2
- AnalyticsEventsNames,
3
- GeneralAppData,
4
- SceneInfo,
5
- AppInfo,
6
- UserInfo,
7
- } from '../../interface';
8
- import type { GAEventsNames } from './interface';
9
-
10
- const MILI_TO_MICRO = 10;
11
-
12
- const camelToSnake = (name: string) => (
13
- name.replace(/[A-Z]/g, (match) => `_${match.toLowerCase()}`)
14
- );
15
-
16
- const addKeyIfExists = <T extends string>(
17
- keyName: T,
18
- payload: { [key in typeof keyName]?: any },
19
- gtagPayload: Record<string, any>,
20
- ) => {
21
- if (payload[keyName]) gtagPayload[camelToSnake(keyName)] = payload[keyName];
22
- };
23
-
24
- const mapGeneralData = (payload: Partial<GeneralAppData>) => {
25
- const gtagPayloadGeneralData: Partial<GeneralAppData> = {};
26
- addKeyIfExists('language', payload, gtagPayloadGeneralData);
27
- addKeyIfExists('resolution', payload, gtagPayloadGeneralData);
28
- return gtagPayloadGeneralData;
29
- };
30
-
31
- const mapSceneInfo = (payload: Partial<SceneInfo>) => {
32
- const gtagPayloadSceneInfo: Partial<SceneInfo> = {};
33
- addKeyIfExists('sceneId', payload, gtagPayloadSceneInfo);
34
- addKeyIfExists('sceneType', payload, gtagPayloadSceneInfo);
35
- addKeyIfExists('sceneName', payload, gtagPayloadSceneInfo);
36
- return gtagPayloadSceneInfo;
37
- };
38
-
39
- const mapAppData = (payload: Partial<AppInfo>) => {
40
- const gtagPayloadAppInfo: Partial<AppInfo> = {};
41
- addKeyIfExists('applicationId', payload, gtagPayloadAppInfo);
42
- addKeyIfExists('serviceId', payload, gtagPayloadAppInfo);
43
- return gtagPayloadAppInfo;
44
- };
45
-
46
- const mapUserData = (payload: Partial<UserInfo>) => {
47
- const gtagPayloadUserInfo: Partial<UserInfo> = {};
48
- addKeyIfExists('userId', payload, gtagPayloadUserInfo);
49
- return gtagPayloadUserInfo;
1
+ import type { AnalyticsEventsNames } from '../../interface';
2
+ import type { EventUrlParams, GAEventsNames, SessionInfo } from './interface';
3
+
4
+ export const mapEventParamsToUrlParams = (eventParams: Record<string, any>) => {
5
+ const urlParams: Partial<EventUrlParams> = {};
6
+ if (eventParams.language) urlParams.ul = eventParams.language;
7
+ if (eventParams.resolution) urlParams.sr = eventParams.resolution;
8
+ if (eventParams.sceneId) urlParams.dl = eventParams.sceneId;
9
+ if (eventParams.sceneName) urlParams.dt = eventParams.sceneName;
10
+ return urlParams as EventUrlParams;
50
11
  };
51
12
 
52
13
  export const mapPayload = (
53
14
  eventName: GAEventsNames,
54
15
  triggerName: AnalyticsEventsNames,
55
- sessionId: string,
56
- payload: Record<string, any>,
16
+ sessionInfo: SessionInfo,
57
17
  ) => {
58
- let gtagPayload: Record<string, any> = {
59
- session_id: sessionId,
60
- event_trigger: triggerName,
61
- timestamp_micros: Date.now() * MILI_TO_MICRO,
62
- ...mapGeneralData(payload),
63
- ...mapAppData(payload),
64
- ...mapUserData(payload),
18
+ const gtagPayload: Record<string, any> = {
19
+ 'ep.event_trigger': triggerName,
20
+ _et: sessionInfo.engagementTime ?? 0,
21
+ _ee: sessionInfo.firstEvent ? 1 : 0,
22
+ _fv: sessionInfo.firstVisit ? 1 : 0,
23
+ _ss: sessionInfo.sessionStarted ? 1 : 0,
65
24
  };
66
- if (eventName === 'page_view' || eventName === 'buffering_start') {
67
- gtagPayload = {
68
- ...gtagPayload,
69
- ...mapSceneInfo(payload),
70
- };
71
- }
72
25
  if (eventName === 'scroll') {
73
- gtagPayload.percent_scroll = parseInt(triggerName.split('_')[1], 10);
26
+ gtagPayload['epn.percent_scroll'] = parseInt(triggerName.split('_')[1], 10);
74
27
  }
75
28
  return gtagPayload;
76
29
  };
@@ -0,0 +1,14 @@
1
+ import { QueuedEvent } from './interface';
2
+
3
+ export const prepareBody = (events: QueuedEvent[]) => {
4
+ const eventLines = events.map((event: QueuedEvent) => {
5
+ const eventParams = [
6
+ `en=${event.name}`,
7
+ ];
8
+ Object.keys(event.params).forEach((paramName) => {
9
+ eventParams.push(`${paramName}=${encodeURIComponent(event.params[paramName])}`);
10
+ });
11
+ return eventParams.join('&');
12
+ });
13
+ return eventLines.join('\r\n');
14
+ };
@@ -61,6 +61,8 @@ export class FirstOnlyGrid<T extends Item<U>, U>
61
61
  const linesDiff = Math.floor(grid.index / itemsInGroup) - oldLine;
62
62
  if (linesDiff > 0) {
63
63
  this.scrollForward(linesDiff);
64
+ } else if (linesDiff < 0) {
65
+ this.scrollBackward(Math.abs(linesDiff));
64
66
  }
65
67
  };
66
68
 
@@ -77,8 +79,8 @@ export class FirstOnlyGrid<T extends Item<U>, U>
77
79
  this.controller.scroll(targetScrollIndex);
78
80
  }
79
81
 
80
- scrollBackward() {
81
- const targetScrollIndex = this.controller.scrollIndex - 1;
82
+ scrollBackward(diff = 1) {
83
+ const targetScrollIndex = this.controller.scrollIndex - diff;
82
84
  this.controller.scroll(targetScrollIndex);
83
85
  }
84
86
 
@@ -1,6 +1,7 @@
1
1
  .keyboard-backdrop {
2
2
  position: fixed;
3
3
  left: 0;
4
+ right: 0;
4
5
  top: 0;
5
6
  width: $backdrop-width;
6
7
  height: $backdrop-height;
@@ -9,6 +10,7 @@
9
10
  .keyboard-backdrop-disabled {
10
11
  position: absolute;
11
12
  left: 0;
13
+ right: 0;
12
14
  top: 0;
13
15
  width: 100%;
14
16
  height: 100%;
@@ -1,6 +1,6 @@
1
1
  import { Component, createRef, DeclareProps } from '@24i/bigscreen-sdk/jsx';
2
2
  import { PlayerBase } from '@24i/player-base';
3
- import { switchByKey } from '@24i/bigscreen-sdk/device/keymap';
3
+ import { FF, REW, switchByKey } from '@24i/bigscreen-sdk/device/keymap';
4
4
  import { stopEvent } from '@24i/bigscreen-sdk/utils/stopEvent';
5
5
  import {
6
6
  IDisplayToggler, getDisplayToggler, DISPLAY_NONE,
@@ -22,6 +22,7 @@ type Props = {
22
22
  hidden?: boolean,
23
23
  onClose?: () => void,
24
24
  onHide?: () => void,
25
+ isPauseAllowed?: () => boolean,
25
26
  };
26
27
 
27
28
  export class PlayerUI extends Component<Props>
@@ -47,6 +48,7 @@ export class PlayerUI extends Component<Props>
47
48
  hidden: false,
48
49
  onClose: noop,
49
50
  onHide: noop,
51
+ isPauseAllowed: () => true,
50
52
  };
51
53
 
52
54
  declare props: DeclareProps<Props, typeof PlayerUI.defaultProps>;
@@ -83,7 +85,7 @@ export class PlayerUI extends Component<Props>
83
85
  FF: this.onKeyForward,
84
86
  REW: this.onKeyRewind,
85
87
  });
86
- if (processed) stopEvent(event);
88
+ if (processed && !FF.is(event) && !REW.is(event)) stopEvent(event);
87
89
  };
88
90
 
89
91
  onKeyPlay = () => {
@@ -101,6 +103,7 @@ export class PlayerUI extends Component<Props>
101
103
  };
102
104
 
103
105
  onKeyPause = () => {
106
+ if (!this.props.isPauseAllowed()) return false;
104
107
  if (this.seeking.isAutomaticallySeeking()) {
105
108
  this.seeking.stopAutomaticSeek();
106
109
  if (this.isPlaying()) this.trigger(PlayerAction.PAUSE);
@@ -115,6 +118,7 @@ export class PlayerUI extends Component<Props>
115
118
  };
116
119
 
117
120
  onKeyPlayPause = () => {
121
+ if (!this.props.isPauseAllowed()) return false;
118
122
  if (this.seeking.isSeeking()) {
119
123
  this.seeking.confirmSeek();
120
124
  if (!this.isPlaying()) this.trigger(PlayerAction.PLAY);
@@ -139,6 +143,7 @@ export class PlayerUI extends Component<Props>
139
143
  }
140
144
  if (!this.seeking.isAutomaticallySeekingForward) {
141
145
  this.seeking.startAutomaticSeekForward();
146
+ this.disableHiding();
142
147
  return true;
143
148
  }
144
149
  return false;
@@ -151,6 +156,7 @@ export class PlayerUI extends Component<Props>
151
156
  }
152
157
  if (!this.seeking.isAutomaticallySeekingBackward) {
153
158
  this.seeking.startAutomaticSeekBackward();
159
+ this.disableHiding();
154
160
  return true;
155
161
  }
156
162
  return false;
@@ -51,6 +51,7 @@ export class Seekbar extends Component<Props> implements IFocusable {
51
51
  document.addEventListener('mouseup', this.onMouseUp);
52
52
  document.addEventListener('mousemove', this.onMouseMove);
53
53
  ui.seeking.addEventListener('seektimeupdate', this.onSeekTimeUpdate);
54
+ ui.seeking.addEventListener('stopautomaticseek', this.onStopAutomaticSeek);
54
55
  ui.getPlayer().addEventListener('timeupdate', this.onTimeUpdate);
55
56
  ui.getPlayer().addEventListener('durationchange', this.onDurationUpdate);
56
57
  }
@@ -60,6 +61,7 @@ export class Seekbar extends Component<Props> implements IFocusable {
60
61
  document.removeEventListener('mouseup', this.onMouseUp);
61
62
  document.removeEventListener('mousemove', this.onMouseMove);
62
63
  ui.seeking.removeEventListener('seektimeupdate', this.onSeekTimeUpdate);
64
+ ui.seeking.removeEventListener('stopautomaticseek', this.onStopAutomaticSeek);
63
65
  ui.getPlayer().removeEventListener('timeupdate', this.onTimeUpdate);
64
66
  ui.getPlayer().removeEventListener('durationchange', this.onDurationUpdate);
65
67
  }
@@ -163,6 +165,8 @@ export class Seekbar extends Component<Props> implements IFocusable {
163
165
  this.updateSeekBar();
164
166
  };
165
167
 
168
+ onStopAutomaticSeek = () => this.props.ui.enableHiding();
169
+
166
170
  onTimeUpdate = ({ currentTime }: ITimeUpdateEvent) => {
167
171
  const { ui } = this.props;
168
172
  this.currentTime = currentTime;
@@ -95,6 +95,7 @@ export class Seeking implements IEvents<SeekingEventsMap> {
95
95
  }
96
96
 
97
97
  stopAutomaticSeek() {
98
+ if (this.isAutomaticallySeeking()) this.triggerEvent('stopautomaticseek');
98
99
  this.automaticSeekInterval.clear();
99
100
  this.isAutomaticallySeekingForward = false;
100
101
  this.isAutomaticallySeekingBackward = false;
@@ -0,0 +1,47 @@
1
+ import { HOUR_IN_MS, MINUTE_IN_MS } from '@24i/bigscreen-sdk/utils';
2
+ import * as TwentyFourIMediaTimeApi from '../services/24iMediaTimeApi';
3
+
4
+ type ParamOptions = TwentyFourIMediaTimeApi.ParamOptions;
5
+
6
+ const defaultOptions: ParamOptions = {
7
+ timeoutInMs: 30000,
8
+ retryConfig: {
9
+ retry: 2,
10
+ },
11
+ };
12
+
13
+ /**
14
+ * Returns a tring in ISO format followed by +/-hh:mm suffix for timezone offset.
15
+ * @param resp Time api response.
16
+ * @returns ISO string with timezone data.
17
+ */
18
+ const getIsoDateString = (resp: TwentyFourIMediaTimeApi.ServiceDataResponse): string => {
19
+ let datetime = new Date(resp.timestamp + resp.timezone_offset).toISOString();
20
+ const offset = resp.timezone_offset / HOUR_IN_MS;
21
+ if (offset) {
22
+ const sign = offset > 0 ? '+' : '-';
23
+ const hours = Math.abs(offset);
24
+ const minutes = Math.floor((resp.timezone_offset % HOUR_IN_MS) / MINUTE_IN_MS);
25
+ // eslint-disable-next-line no-magic-numbers
26
+ const timeString = sign + `0${hours}`.slice(-2) + ':' + `0${minutes}`.slice(-2);
27
+ datetime = datetime.replace('Z', timeString);
28
+ }
29
+ return datetime;
30
+ };
31
+
32
+ /**
33
+ * Wrapper over "TwentyFourIMediaTimeApi.getTime" method requesting the current time
34
+ * from timekeeping server. Transforms the result to ISO string with timezone offset data
35
+ * included.
36
+ *
37
+ * @param options Options object
38
+ */
39
+ export const getTime = async (options: ParamOptions = {}) => {
40
+ const opts = { ...defaultOptions, ...options };
41
+ const resp = await TwentyFourIMediaTimeApi.getTime(opts);
42
+
43
+ if ('timestamp' in resp && 'timezone_offset' in resp && 'in_dst' in resp) {
44
+ return { datetime: getIsoDateString(resp) };
45
+ }
46
+ return null;
47
+ };
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Package "24iMediaTimeApi" provides a single function to obtain the current time
3
+ * adjusted by local timezone and daylight savings time for use in time package,
4
+ * from 24i time API at https://time.24imedia.tv/
5
+ */
6
+ import {
7
+ xhrSendRetry, type RetryConfig, type XhrResponse,
8
+ } from '@24i/bigscreen-sdk/utils/xhr';
9
+
10
+ const API_URL = 'https://time.24imedia.tv/';
11
+
12
+ export type Options = {
13
+ /** The request timeout in milliseconds. The value 0 for no timeout */
14
+ timeoutInMs: number,
15
+ /** Retry configuration */
16
+ retryConfig: RetryConfig,
17
+ };
18
+ export type ParamOptions = Partial<Options>;
19
+
20
+ export type ServiceDataResponse = {
21
+ /** Is dailight savings time */
22
+ in_dst: boolean,
23
+ /** Current time in GMT, in milliseconds */
24
+ timestamp: number,
25
+ /** Timezeone offset in milliseconds */
26
+ timezone_offset: number,
27
+ };
28
+
29
+ /**
30
+ * Request the current time based on public IP of request.
31
+ *
32
+ * @param options Options object
33
+ */
34
+ export async function getTime(options?: ParamOptions): Promise<ServiceDataResponse> {
35
+ try {
36
+ const resp = (await xhrSendRetry(API_URL, options)) as XhrResponse;
37
+ const data: any = resp.dataJson || {};
38
+ return data as ServiceDataResponse;
39
+ } catch (e) {
40
+ console.error('[24iMediaTimeApi] Failed to get time', e);
41
+ throw e;
42
+ }
43
+ }
@@ -0,0 +1,6 @@
1
+ export {
2
+ Options,
3
+ ParamOptions,
4
+ ServiceDataResponse,
5
+ getTime,
6
+ } from './24iMediaTimeApi';
@@ -9,6 +9,7 @@ const trimWithEllipsis = (
9
9
  if (!row.textContent) {
10
10
  return;
11
11
  }
12
+ const { visibility } = row.style;
12
13
  row.style.visibility = 'hidden';
13
14
  const lineHeightString = window.getComputedStyle(row, null).getPropertyValue('line-height');
14
15
  const lineHeight = parseInt(lineHeightString, 10);
@@ -27,6 +28,7 @@ const trimWithEllipsis = (
27
28
  } while (numberOfLines <= requestedNumberOfLines
28
29
  && currentPosition < originalTextContent.length);
29
30
  row.textContent = stringBuffer.substring(0, stringBuffer.length - 1) + ellipsis;
31
+
30
32
  } else {
31
33
  let currentPosition = originalTextContent.length - 1;
32
34
  do {
@@ -39,7 +41,7 @@ const trimWithEllipsis = (
39
41
  row.textContent = `${ellipsis}${stringBuffer.substring(1)}`;
40
42
  }
41
43
  }
42
- row.style.visibility = '';
44
+ row.style.visibility = visibility;
43
45
  };
44
46
 
45
47
  export const trimWithLeadingEllipsis = (