@applicaster/zapp-react-native-utils 13.0.10-alpha.9045951059 → 13.0.10-rc.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/appUtils/accessibilityManager/const.ts +0 -13
- package/appUtils/accessibilityManager/hooks.ts +1 -35
- package/appUtils/accessibilityManager/index.ts +28 -149
- package/appUtils/platform/platformUtils.ts +1 -31
- package/index.d.ts +1 -4
- package/package.json +2 -2
- package/appUtils/accessibilityManager/utils.ts +0 -24
- package/playerUtils/PlayerTTS/PlayerTTS.ts +0 -359
- package/playerUtils/PlayerTTS/index.ts +0 -1
- package/playerUtils/usePlayerTTS.ts +0 -21
|
@@ -65,17 +65,4 @@ export const BUTTON_ACCESSIBILITY_KEYS = {
|
|
|
65
65
|
label: "accessibility_menu_item_label",
|
|
66
66
|
hint: "accessibility_menu_item_hint",
|
|
67
67
|
},
|
|
68
|
-
skip_intro: {
|
|
69
|
-
label: "accessibility_skip_intro_label",
|
|
70
|
-
hint: "accessibility_skip_intro_hint",
|
|
71
|
-
},
|
|
72
|
-
// Top Menu Bar-specific buttons
|
|
73
|
-
top_menu_bar_item_selected: {
|
|
74
|
-
label: "accessibility_top_menu_bar_item_selected_label",
|
|
75
|
-
hint: "accessibility_top_menu_bar_item_selected_hint",
|
|
76
|
-
},
|
|
77
|
-
top_menu_title: {
|
|
78
|
-
label: "accessibility_top_menu_title_label",
|
|
79
|
-
hint: "accessibility_top_menu_hint",
|
|
80
|
-
},
|
|
81
68
|
} as const;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useMemo
|
|
1
|
+
import { useEffect, useMemo } from "react";
|
|
2
2
|
import { AccessibilityManager } from "./index";
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -38,37 +38,3 @@ export const useAccessibilityManager = (
|
|
|
38
38
|
|
|
39
39
|
return accessibilityManager;
|
|
40
40
|
};
|
|
41
|
-
|
|
42
|
-
export const useInitialAnnouncementReady = (
|
|
43
|
-
accessibilityManager: AccessibilityManager
|
|
44
|
-
) => {
|
|
45
|
-
const [isReady, setIsReady] = useState(
|
|
46
|
-
accessibilityManager.isInitialPlayerAnnouncementReady
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
useEffect(() => {
|
|
50
|
-
const subscription = accessibilityManager
|
|
51
|
-
.getInitialAnnouncementReadyObservable()
|
|
52
|
-
.subscribe(setIsReady);
|
|
53
|
-
|
|
54
|
-
return () => subscription.unsubscribe();
|
|
55
|
-
}, [accessibilityManager]);
|
|
56
|
-
|
|
57
|
-
return isReady;
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
export const useAnnouncementActive = (
|
|
61
|
-
accessibilityManager: AccessibilityManager
|
|
62
|
-
) => {
|
|
63
|
-
const [isActive, setIsActive] = useState(false);
|
|
64
|
-
|
|
65
|
-
useEffect(() => {
|
|
66
|
-
const subscription = accessibilityManager
|
|
67
|
-
.getTTSStateObservable()
|
|
68
|
-
.subscribe(setIsActive);
|
|
69
|
-
|
|
70
|
-
return () => subscription.unsubscribe();
|
|
71
|
-
}, [accessibilityManager]);
|
|
72
|
-
|
|
73
|
-
return isActive;
|
|
74
|
-
};
|
|
@@ -1,19 +1,15 @@
|
|
|
1
|
-
import * as R from "ramda";
|
|
2
1
|
import { BehaviorSubject } from "rxjs";
|
|
3
2
|
import { accessibilityManagerLogger as logger } from "./logger";
|
|
4
3
|
import { TTSManager } from "../platform/platformUtils";
|
|
5
4
|
import { BUTTON_ACCESSIBILITY_KEYS } from "./const";
|
|
6
|
-
import { calculateReadingTime } from "./utils";
|
|
7
5
|
import { AccessibilityRole } from "react-native";
|
|
8
6
|
import _ from "lodash";
|
|
9
7
|
|
|
10
8
|
export class AccessibilityManager {
|
|
11
9
|
private static _instance: AccessibilityManager | null = null;
|
|
12
10
|
private headingTimeout: NodeJS.Timeout | null = null;
|
|
13
|
-
private
|
|
14
|
-
private WORDS_PER_MINUTE = 140;
|
|
11
|
+
private WORDS_PER_MINUTE = 160;
|
|
15
12
|
private MINIMUM_PAUSE = 500;
|
|
16
|
-
private ANNOUNCEMENT_DELAY = 700;
|
|
17
13
|
private state$ = new BehaviorSubject<AccessibilityState>({
|
|
18
14
|
screenReaderEnabled: false,
|
|
19
15
|
reduceMotionEnabled: false,
|
|
@@ -29,12 +25,6 @@ export class AccessibilityManager {
|
|
|
29
25
|
private ttsManager = TTSManager.getInstance();
|
|
30
26
|
private localizations: { [key: string]: string } = {};
|
|
31
27
|
private headingQueue: string[] = [];
|
|
32
|
-
private currentFocusId: string | null = null;
|
|
33
|
-
private headingFocusMap: Map<string, string> = new Map();
|
|
34
|
-
private pendingFocusId: string | null = null;
|
|
35
|
-
private isInitialPlayerAnnouncementReady$ = new BehaviorSubject<boolean>(
|
|
36
|
-
false
|
|
37
|
-
);
|
|
38
28
|
|
|
39
29
|
private constructor() {}
|
|
40
30
|
|
|
@@ -46,26 +36,6 @@ export class AccessibilityManager {
|
|
|
46
36
|
return AccessibilityManager._instance;
|
|
47
37
|
}
|
|
48
38
|
|
|
49
|
-
public get isInitialPlayerAnnouncementReady(): boolean {
|
|
50
|
-
return this.isInitialPlayerAnnouncementReady$.getValue();
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
public setInitialPlayerAnnouncementReady(): void {
|
|
54
|
-
this.isInitialPlayerAnnouncementReady$.next(true);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
public resetInitialPlayerAnnouncementReady(): void {
|
|
58
|
-
this.isInitialPlayerAnnouncementReady$.next(false);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
public getInitialAnnouncementReadyObservable() {
|
|
62
|
-
return this.isInitialPlayerAnnouncementReady$.asObservable();
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
public getTTSStateObservable() {
|
|
66
|
-
return this.ttsManager.getStateAsObservable();
|
|
67
|
-
}
|
|
68
|
-
|
|
69
39
|
/**
|
|
70
40
|
* The method now accepts any object with localizations using a flattened structure
|
|
71
41
|
*
|
|
@@ -76,9 +46,7 @@ export class AccessibilityManager {
|
|
|
76
46
|
* i.e. localizations: [{ en: { accessibility_close_label: "Close", accessibility_close_hint: "Press here to close" } }]
|
|
77
47
|
*/
|
|
78
48
|
public updateLocalizations(localizations: { [key: string]: string }) {
|
|
79
|
-
|
|
80
|
-
this.localizations = localizations;
|
|
81
|
-
}
|
|
49
|
+
this.localizations = localizations;
|
|
82
50
|
}
|
|
83
51
|
|
|
84
52
|
public getState(): AccessibilityState {
|
|
@@ -89,25 +57,31 @@ export class AccessibilityManager {
|
|
|
89
57
|
return this.state$.asObservable();
|
|
90
58
|
}
|
|
91
59
|
|
|
60
|
+
/** Calculates the reading time for a given text
|
|
61
|
+
* This method is a bit of a hack because we don't have a callback, or promise from VIZIO API
|
|
62
|
+
* @param text - The text to calculate the reading time for
|
|
63
|
+
* @returns The reading time in milliseconds
|
|
64
|
+
*/
|
|
65
|
+
private calculateReadingTime(text: string): number {
|
|
66
|
+
const words = text.trim().split(/\s+/).length;
|
|
67
|
+
|
|
68
|
+
return Math.max(
|
|
69
|
+
this.MINIMUM_PAUSE,
|
|
70
|
+
(words / this.WORDS_PER_MINUTE) * 60 * 1000
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
92
74
|
/**
|
|
93
75
|
* Adds a heading to the queue, headings will be read before the next text
|
|
94
76
|
* Each heading will be read once and removed from the queue
|
|
95
77
|
*/
|
|
96
78
|
public addHeading(heading: string) {
|
|
97
|
-
if (!this.pendingFocusId) {
|
|
98
|
-
this.pendingFocusId = Date.now().toString();
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
this.headingFocusMap.set(heading, this.pendingFocusId);
|
|
102
79
|
this.headingQueue.push(heading);
|
|
103
80
|
}
|
|
104
81
|
|
|
105
82
|
/**
|
|
106
83
|
* text you want to be read, if you want to use localized text pass keyOfLocalizedText instead
|
|
107
84
|
* keyOfLocalizedText is the key to the localized text
|
|
108
|
-
*
|
|
109
|
-
* Implements a delay mechanism to reduce noise during rapid navigation.
|
|
110
|
-
* Only the most recent announcement will be read after the delay period.
|
|
111
85
|
*/
|
|
112
86
|
public readText({
|
|
113
87
|
text,
|
|
@@ -138,27 +112,19 @@ export class AccessibilityManager {
|
|
|
138
112
|
textToRead = localizedMessage;
|
|
139
113
|
}
|
|
140
114
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
115
|
+
if (this.headingQueue.length > 0) {
|
|
116
|
+
const heading = this.headingQueue.shift()!;
|
|
117
|
+
this.ttsManager?.readText(heading);
|
|
144
118
|
|
|
145
|
-
|
|
119
|
+
if (this.headingTimeout) {
|
|
120
|
+
clearTimeout(this.headingTimeout);
|
|
121
|
+
}
|
|
146
122
|
|
|
147
|
-
|
|
148
|
-
this.executeAnnouncement(textToRead, keyOfLocalizedText, focusId);
|
|
149
|
-
}, this.ANNOUNCEMENT_DELAY);
|
|
150
|
-
}
|
|
123
|
+
const pauseTime = this.calculateReadingTime(heading);
|
|
151
124
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
private executeAnnouncement(
|
|
156
|
-
textToRead: string,
|
|
157
|
-
keyOfLocalizedText?: string,
|
|
158
|
-
focusId?: string
|
|
159
|
-
) {
|
|
160
|
-
if (this.headingQueue.length > 0) {
|
|
161
|
-
this.processHeadingQueue(textToRead, focusId);
|
|
125
|
+
this.headingTimeout = setTimeout(() => {
|
|
126
|
+
this.ttsManager?.readText(textToRead);
|
|
127
|
+
}, pauseTime);
|
|
162
128
|
} else {
|
|
163
129
|
this.ttsManager?.readText(textToRead);
|
|
164
130
|
}
|
|
@@ -170,54 +136,6 @@ export class AccessibilityManager {
|
|
|
170
136
|
});
|
|
171
137
|
}
|
|
172
138
|
|
|
173
|
-
/**
|
|
174
|
-
* Recursively processes all headings in the queue, reading them one by one
|
|
175
|
-
*/
|
|
176
|
-
private processHeadingQueue(textToRead: string, focusId?: string) {
|
|
177
|
-
// If focus has changed, abort this announcement
|
|
178
|
-
if (focusId && this.currentFocusId !== focusId) {
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (this.headingQueue.length === 0) {
|
|
183
|
-
if (focusId && this.currentFocusId === focusId) {
|
|
184
|
-
this.ttsManager?.readText(textToRead);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const heading = this.headingQueue.shift()!;
|
|
191
|
-
|
|
192
|
-
const headingFocusId = this.headingFocusMap.get(heading);
|
|
193
|
-
|
|
194
|
-
if (headingFocusId && headingFocusId !== focusId) {
|
|
195
|
-
// This heading belongs to a previous focus, skip it
|
|
196
|
-
this.headingFocusMap.delete(heading);
|
|
197
|
-
this.processHeadingQueue(textToRead, focusId);
|
|
198
|
-
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
this.ttsManager?.readText(heading);
|
|
203
|
-
this.headingFocusMap.delete(heading); // Clean up after reading
|
|
204
|
-
|
|
205
|
-
if (this.headingTimeout) {
|
|
206
|
-
clearTimeout(this.headingTimeout);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const pauseTime = calculateReadingTime(
|
|
210
|
-
heading,
|
|
211
|
-
this.WORDS_PER_MINUTE,
|
|
212
|
-
this.MINIMUM_PAUSE,
|
|
213
|
-
this.ANNOUNCEMENT_DELAY
|
|
214
|
-
);
|
|
215
|
-
|
|
216
|
-
this.headingTimeout = setTimeout(() => {
|
|
217
|
-
this.processHeadingQueue(textToRead, focusId);
|
|
218
|
-
}, pauseTime);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
139
|
public getButtonAccessibilityProps(name: string): AccessibilityProps {
|
|
222
140
|
const buttonName = _.toString(name);
|
|
223
141
|
|
|
@@ -225,15 +143,12 @@ export class AccessibilityManager {
|
|
|
225
143
|
|
|
226
144
|
if (!buttonConfig) {
|
|
227
145
|
return {
|
|
228
|
-
accessible: true,
|
|
229
146
|
accessibilityLabel: buttonName,
|
|
230
147
|
accessibilityHint: `Press button to perform action on ${buttonName}`,
|
|
231
148
|
"aria-label": buttonName,
|
|
232
149
|
"aria-description": `Press button to perform action on ${buttonName}`,
|
|
233
150
|
accessibilityRole: "button" as AccessibilityRole,
|
|
234
151
|
"aria-role": "button",
|
|
235
|
-
role: "button",
|
|
236
|
-
tabindex: 0,
|
|
237
152
|
};
|
|
238
153
|
}
|
|
239
154
|
|
|
@@ -247,52 +162,23 @@ export class AccessibilityManager {
|
|
|
247
162
|
`Press button to perform action on ${buttonName}`;
|
|
248
163
|
|
|
249
164
|
return {
|
|
250
|
-
accessible: true,
|
|
251
165
|
accessibilityLabel: label,
|
|
252
166
|
accessibilityHint: hint,
|
|
253
167
|
"aria-label": label,
|
|
254
168
|
"aria-description": hint,
|
|
255
169
|
accessibilityRole: "button" as AccessibilityRole,
|
|
256
170
|
"aria-role": "button",
|
|
257
|
-
role: "button",
|
|
258
|
-
tabindex: 0,
|
|
259
171
|
};
|
|
260
172
|
}
|
|
261
173
|
|
|
262
174
|
public getInputAccessibilityProps(inputName: string): AccessibilityProps {
|
|
263
175
|
return {
|
|
264
|
-
accessible: true,
|
|
265
176
|
accessibilityLabel: inputName,
|
|
266
177
|
accessibilityHint: `Enter text into ${inputName}`,
|
|
267
178
|
"aria-label": inputName,
|
|
268
179
|
"aria-description": `Enter text into ${inputName}`,
|
|
269
|
-
accessibilityRole: "
|
|
270
|
-
"aria-role": "
|
|
271
|
-
role: "text",
|
|
272
|
-
tabindex: 0,
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* Extracts accessibility props from component props and returns them as HTML attributes
|
|
278
|
-
* @param props - Component props containing accessibility properties
|
|
279
|
-
* @returns Object with accessibility HTML attributes
|
|
280
|
-
*/
|
|
281
|
-
public getWebAccessibilityProps(props: any): AccessibilityProps {
|
|
282
|
-
const {
|
|
283
|
-
"aria-label": ariaLabel,
|
|
284
|
-
"aria-description": ariaDescription,
|
|
285
|
-
"aria-role": ariaRole,
|
|
286
|
-
role,
|
|
287
|
-
tabindex,
|
|
288
|
-
} = props;
|
|
289
|
-
|
|
290
|
-
return {
|
|
291
|
-
"aria-label": ariaLabel,
|
|
292
|
-
"aria-description": ariaDescription,
|
|
293
|
-
"aria-role": ariaRole,
|
|
294
|
-
role: role || ariaRole,
|
|
295
|
-
tabindex,
|
|
180
|
+
accessibilityRole: "textbox" as AccessibilityRole,
|
|
181
|
+
"aria-role": "textbox",
|
|
296
182
|
};
|
|
297
183
|
}
|
|
298
184
|
|
|
@@ -310,11 +196,4 @@ export class AccessibilityManager {
|
|
|
310
196
|
|
|
311
197
|
return this.localizations[key];
|
|
312
198
|
}
|
|
313
|
-
|
|
314
|
-
private clearAnnouncement() {
|
|
315
|
-
if (this.announcementDelayTimeout) {
|
|
316
|
-
clearTimeout(this.announcementDelayTimeout);
|
|
317
|
-
this.announcementDelayTimeout = null;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
199
|
}
|
|
@@ -10,7 +10,6 @@ import { PLATFORM_KEYS, PLATFORMS, ZappPlatform } from "./const";
|
|
|
10
10
|
import { createLogger, utilsLogger } from "../../logger";
|
|
11
11
|
import { getPlatform } from "../../reactUtils";
|
|
12
12
|
import { appStore } from "@applicaster/zapp-react-native-redux/AppStore";
|
|
13
|
-
import { calculateReadingTime } from "@applicaster/zapp-react-native-utils/appUtils/accessibilityManager/utils";
|
|
14
13
|
|
|
15
14
|
const { log_debug } = createLogger({
|
|
16
15
|
category: "General",
|
|
@@ -213,16 +212,9 @@ export class TTSManager {
|
|
|
213
212
|
}
|
|
214
213
|
|
|
215
214
|
readText(text: string) {
|
|
216
|
-
this.ttsState$.next(true);
|
|
217
|
-
|
|
218
215
|
if (isSamsungPlatform() && window.speechSynthesis) {
|
|
219
216
|
const utterance = new SpeechSynthesisUtterance(text);
|
|
220
|
-
|
|
221
|
-
window.speechSynthesis.cancel(); // Cancel previous speech before speaking new text
|
|
222
217
|
window.speechSynthesis.speak(utterance);
|
|
223
|
-
|
|
224
|
-
// Estimate reading time and set inactive when done
|
|
225
|
-
this.scheduleTTSComplete(text);
|
|
226
218
|
}
|
|
227
219
|
|
|
228
220
|
if (isLgPlatform() && window.webOS?.service) {
|
|
@@ -233,45 +225,23 @@ export class TTSManager {
|
|
|
233
225
|
log_debug("There was a failure setting up webOS TTS service", {
|
|
234
226
|
error,
|
|
235
227
|
});
|
|
236
|
-
|
|
237
|
-
this.ttsState$.next(false);
|
|
238
228
|
},
|
|
239
229
|
onSuccess(response: any) {
|
|
240
230
|
log_debug("webOS TTS service is configured successfully", {
|
|
241
231
|
response,
|
|
242
232
|
});
|
|
243
|
-
|
|
244
|
-
// Estimate reading time and set inactive when done
|
|
245
|
-
this.scheduleTTSComplete(text);
|
|
246
233
|
},
|
|
247
234
|
parameters: {
|
|
248
235
|
text,
|
|
249
|
-
clear: true, // Clear any previous speech before speaking new text
|
|
250
236
|
},
|
|
251
237
|
});
|
|
252
238
|
} catch (error) {
|
|
253
239
|
log_debug("webOS TTS service error", { error });
|
|
254
|
-
this.ttsState$.next(false);
|
|
255
240
|
}
|
|
256
241
|
}
|
|
257
242
|
|
|
258
|
-
if (!window.VIZIO?.Chromevox)
|
|
259
|
-
// For platforms without TTS, estimate reading time
|
|
260
|
-
this.scheduleTTSComplete(text);
|
|
261
|
-
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
243
|
+
if (!window.VIZIO?.Chromevox) return;
|
|
264
244
|
|
|
265
245
|
window.VIZIO.Chromevox.play(text);
|
|
266
|
-
// Estimate reading time and set inactive when done
|
|
267
|
-
this.scheduleTTSComplete(text);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
private scheduleTTSComplete(text: string) {
|
|
271
|
-
const readingTime = calculateReadingTime(text);
|
|
272
|
-
|
|
273
|
-
setTimeout(() => {
|
|
274
|
-
this.ttsState$.next(false);
|
|
275
|
-
}, readingTime);
|
|
276
246
|
}
|
|
277
247
|
}
|
package/index.d.ts
CHANGED
|
@@ -69,7 +69,7 @@ declare type ExtraProps = ZappUIComponentProps & {
|
|
|
69
69
|
};
|
|
70
70
|
|
|
71
71
|
declare type WebConfirmationDialog = {
|
|
72
|
-
message
|
|
72
|
+
message: string;
|
|
73
73
|
confirmCompletion?: () => void;
|
|
74
74
|
cancelCompletion?: () => void;
|
|
75
75
|
};
|
|
@@ -139,13 +139,10 @@ declare type AccessibilityState = {
|
|
|
139
139
|
};
|
|
140
140
|
|
|
141
141
|
declare type AccessibilityProps = {
|
|
142
|
-
accessible?: boolean;
|
|
143
142
|
accessibilityLabel?: string;
|
|
144
143
|
accessibilityHint?: string;
|
|
145
144
|
"aria-label"?: string;
|
|
146
145
|
"aria-description"?: string;
|
|
147
146
|
accessibilityRole?: string;
|
|
148
147
|
"aria-role"?: string;
|
|
149
|
-
role?: string;
|
|
150
|
-
tabindex?: number;
|
|
151
148
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applicaster/zapp-react-native-utils",
|
|
3
|
-
"version": "13.0.10-
|
|
3
|
+
"version": "13.0.10-rc.0",
|
|
4
4
|
"description": "Applicaster Zapp React Native utilities package",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"homepage": "https://github.com/applicaster/quickbrick#readme",
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@applicaster/applicaster-types": "13.0.10-
|
|
30
|
+
"@applicaster/applicaster-types": "13.0.10-rc.0",
|
|
31
31
|
"buffer": "^5.2.1",
|
|
32
32
|
"camelize": "^1.0.0",
|
|
33
33
|
"dayjs": "^1.11.10",
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Calculates the reading time for a given text based on word count
|
|
3
|
-
* @param text - The text to calculate the reading time for
|
|
4
|
-
* @param wordsPerMinute - Words per minute reading speed (default: 160)
|
|
5
|
-
* @param minimumPause - Minimum pause time in milliseconds (default: 500)
|
|
6
|
-
* @param announcementDelay - Additional delay for announcement in milliseconds (default: 700)
|
|
7
|
-
* @returns The reading time in milliseconds
|
|
8
|
-
*/
|
|
9
|
-
export function calculateReadingTime(
|
|
10
|
-
text: string,
|
|
11
|
-
wordsPerMinute: number = 140,
|
|
12
|
-
minimumPause: number = 500,
|
|
13
|
-
announcementDelay: number = 700
|
|
14
|
-
): number {
|
|
15
|
-
const words = text
|
|
16
|
-
.trim()
|
|
17
|
-
.split(/(?<=\d)(?=[a-zA-Z])|(?<=[a-zA-Z])(?=\d)|[^\w\s]+|\s+/)
|
|
18
|
-
.filter(Boolean).length;
|
|
19
|
-
|
|
20
|
-
return (
|
|
21
|
-
Math.max(minimumPause, (words / wordsPerMinute) * 60 * 1000) +
|
|
22
|
-
announcementDelay
|
|
23
|
-
);
|
|
24
|
-
}
|
|
@@ -1,359 +0,0 @@
|
|
|
1
|
-
import uuidv4 from "uuid/v4";
|
|
2
|
-
import { AccessibilityManager } from "@applicaster/zapp-react-native-utils/appUtils/accessibilityManager";
|
|
3
|
-
import { createLogger } from "@applicaster/zapp-react-native-utils/logger";
|
|
4
|
-
import { Player } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/player";
|
|
5
|
-
|
|
6
|
-
const { log_debug, log_error } = createLogger({
|
|
7
|
-
subsystem: "Player",
|
|
8
|
-
category: "PlayerTTS",
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
enum SEEK_DIRECTION {
|
|
12
|
-
FORWARD = "forward",
|
|
13
|
-
REWIND = "back",
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const hasPrerollAds = (entry: ZappEntry): boolean => {
|
|
17
|
-
const videoAds = entry?.extensions?.video_ads;
|
|
18
|
-
|
|
19
|
-
if (!videoAds) {
|
|
20
|
-
return false;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// If it's a string (VMAP URL), assume it might have preroll
|
|
24
|
-
if (typeof videoAds === "string") {
|
|
25
|
-
return true;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// If it's an array, check for preroll offset
|
|
29
|
-
if (Array.isArray(videoAds)) {
|
|
30
|
-
return videoAds.some(
|
|
31
|
-
(ad: ZappVideoAdExtension) => ad.offset === "preroll" || ad.offset === 0
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return false;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
export class PlayerTTS {
|
|
39
|
-
private player: Player;
|
|
40
|
-
private accessibilityManager: AccessibilityManager;
|
|
41
|
-
private seekStartPosition: number | null = null;
|
|
42
|
-
private isSeeking: boolean = false;
|
|
43
|
-
private listenerId: string;
|
|
44
|
-
private isInitialPlayerOpen: boolean = true;
|
|
45
|
-
private isPrerollActive: boolean = false;
|
|
46
|
-
private hasPrerollAds: boolean = false; // Track if preroll ads are expected
|
|
47
|
-
|
|
48
|
-
constructor(player: Player, accessibilityManager: AccessibilityManager) {
|
|
49
|
-
this.player = player;
|
|
50
|
-
this.accessibilityManager = accessibilityManager;
|
|
51
|
-
this.listenerId = `player-tts-${uuidv4()}`;
|
|
52
|
-
this.hasPrerollAds = hasPrerollAds(player.entry);
|
|
53
|
-
|
|
54
|
-
log_debug("PlayerTTS initialized", {
|
|
55
|
-
hasPrerollAds: this.hasPrerollAds,
|
|
56
|
-
listenerId: this.listenerId,
|
|
57
|
-
entryTitle: player.entry.title,
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
private numberToWords(num: number): string {
|
|
62
|
-
const ones = [
|
|
63
|
-
"",
|
|
64
|
-
"one",
|
|
65
|
-
"two",
|
|
66
|
-
"three",
|
|
67
|
-
"four",
|
|
68
|
-
"five",
|
|
69
|
-
"six",
|
|
70
|
-
"seven",
|
|
71
|
-
"eight",
|
|
72
|
-
"nine",
|
|
73
|
-
"ten",
|
|
74
|
-
"eleven",
|
|
75
|
-
"twelve",
|
|
76
|
-
"thirteen",
|
|
77
|
-
"fourteen",
|
|
78
|
-
"fifteen",
|
|
79
|
-
"sixteen",
|
|
80
|
-
"seventeen",
|
|
81
|
-
"eighteen",
|
|
82
|
-
"nineteen",
|
|
83
|
-
];
|
|
84
|
-
|
|
85
|
-
const tens = [
|
|
86
|
-
"",
|
|
87
|
-
"",
|
|
88
|
-
"twenty",
|
|
89
|
-
"thirty",
|
|
90
|
-
"forty",
|
|
91
|
-
"fifty",
|
|
92
|
-
"sixty",
|
|
93
|
-
"seventy",
|
|
94
|
-
"eighty",
|
|
95
|
-
"ninety",
|
|
96
|
-
];
|
|
97
|
-
|
|
98
|
-
if (num === 0) return "zero";
|
|
99
|
-
if (num < 20) return ones[num];
|
|
100
|
-
|
|
101
|
-
const ten = Math.floor(num / 10);
|
|
102
|
-
const one = num % 10;
|
|
103
|
-
|
|
104
|
-
return one === 0 ? tens[ten] : `${tens[ten]} ${ones[one]}`;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
private secondsToTime(
|
|
108
|
-
seconds: number,
|
|
109
|
-
format: "natural" | "standard" = "natural"
|
|
110
|
-
): string {
|
|
111
|
-
if (seconds < 0) return format === "natural" ? "zero" : "0";
|
|
112
|
-
|
|
113
|
-
const minutes = Math.floor(seconds / 60);
|
|
114
|
-
const remainingSeconds = Math.floor(seconds % 60);
|
|
115
|
-
|
|
116
|
-
if (format === "standard") {
|
|
117
|
-
const parts = [];
|
|
118
|
-
|
|
119
|
-
if (minutes > 0) {
|
|
120
|
-
parts.push(`${minutes} minute${minutes !== 1 ? "s" : ""}`);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (remainingSeconds > 0) {
|
|
124
|
-
parts.push(
|
|
125
|
-
`${remainingSeconds} second${remainingSeconds !== 1 ? "s" : ""}`
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return parts.length > 0 ? parts.join(" ") : "0";
|
|
130
|
-
} else {
|
|
131
|
-
if (minutes === 0) {
|
|
132
|
-
if (remainingSeconds === 0) return "zero";
|
|
133
|
-
|
|
134
|
-
if (remainingSeconds < 10) {
|
|
135
|
-
return `zero o ${this.numberToWords(remainingSeconds)}`;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return `zero ${this.numberToWords(remainingSeconds)}`;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (remainingSeconds === 0) {
|
|
142
|
-
return `${this.numberToWords(minutes)}`;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (remainingSeconds < 10) {
|
|
146
|
-
return `${this.numberToWords(minutes)} o ${this.numberToWords(remainingSeconds)}`;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return `${this.numberToWords(minutes)} ${this.numberToWords(remainingSeconds)}`;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
private announcePause = () => {
|
|
154
|
-
if (!this.isSeeking) {
|
|
155
|
-
this.accessibilityManager.addHeading(
|
|
156
|
-
`Paused - ${this.secondsToTime(this.player.playerState.contentPosition, "standard")}`
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
private announceContentStart(
|
|
162
|
-
options: {
|
|
163
|
-
currentTime?: number;
|
|
164
|
-
duration?: number;
|
|
165
|
-
useReadText?: boolean;
|
|
166
|
-
} = {}
|
|
167
|
-
): void {
|
|
168
|
-
const { currentTime, duration, useReadText = false } = options;
|
|
169
|
-
const state = this.player.playerState;
|
|
170
|
-
|
|
171
|
-
const timeRemaining =
|
|
172
|
-
(duration || state?.contentDuration || 0) -
|
|
173
|
-
(currentTime || state?.contentPosition || 0);
|
|
174
|
-
|
|
175
|
-
const title = (this.player.entry.title as string) || "";
|
|
176
|
-
const summary = (this.player.entry.summary as string) || "";
|
|
177
|
-
|
|
178
|
-
log_debug("Announcing content start", {
|
|
179
|
-
title,
|
|
180
|
-
currentTime: currentTime || state?.contentPosition || 0,
|
|
181
|
-
duration: duration || state?.contentDuration || 0,
|
|
182
|
-
timeRemaining,
|
|
183
|
-
useReadText,
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
this.accessibilityManager.addHeading(`Playing - ${title}`);
|
|
187
|
-
if (summary) this.accessibilityManager.addHeading(summary);
|
|
188
|
-
|
|
189
|
-
this.accessibilityManager.addHeading(
|
|
190
|
-
`Playing from ${this.secondsToTime(currentTime || state?.contentPosition || 0, "standard")}`
|
|
191
|
-
);
|
|
192
|
-
|
|
193
|
-
const remainingText = `${this.secondsToTime(Math.max(0, Math.floor(timeRemaining)), "standard")} remaining.`;
|
|
194
|
-
|
|
195
|
-
if (useReadText) {
|
|
196
|
-
this.accessibilityManager.readText({ text: remainingText });
|
|
197
|
-
} else {
|
|
198
|
-
this.accessibilityManager.addHeading(remainingText);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
this.accessibilityManager.setInitialPlayerAnnouncementReady();
|
|
202
|
-
this.isInitialPlayerOpen = false;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
private announceBufferComplete = (event: any) => {
|
|
206
|
-
// If preroll ads are expected, wait for them to finish before announcing content
|
|
207
|
-
if (this.hasPrerollAds && this.isInitialPlayerOpen) {
|
|
208
|
-
log_debug("Waiting for preroll ads to finish", {
|
|
209
|
-
hasPrerollAds: this.hasPrerollAds,
|
|
210
|
-
isInitialPlayerOpen: this.isInitialPlayerOpen,
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Gate content announcement until preroll finishes
|
|
217
|
-
if (this.isInitialPlayerOpen && !this.isPrerollActive) {
|
|
218
|
-
log_debug("Buffer complete - announcing content", {
|
|
219
|
-
currentTime: event.currentTime,
|
|
220
|
-
duration: event.duration,
|
|
221
|
-
isPrerollActive: this.isPrerollActive,
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
this.announceContentStart({
|
|
225
|
-
currentTime: event.currentTime,
|
|
226
|
-
duration: event.duration,
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
private announceResume = () => {
|
|
232
|
-
if (!this.isSeeking && !this.isInitialPlayerOpen) {
|
|
233
|
-
log_debug("Player resumed", {
|
|
234
|
-
contentPosition: this.player.playerState.contentPosition,
|
|
235
|
-
isSeeking: this.isSeeking,
|
|
236
|
-
isInitialPlayerOpen: this.isInitialPlayerOpen,
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
this.accessibilityManager.addHeading(
|
|
240
|
-
`Playing - ${this.secondsToTime(this.player.playerState.contentPosition, "standard")}`
|
|
241
|
-
);
|
|
242
|
-
}
|
|
243
|
-
};
|
|
244
|
-
|
|
245
|
-
private handleVideoProgress = (event: any) => {
|
|
246
|
-
if (event.currentTime > 0) {
|
|
247
|
-
this.seekStartPosition = event.currentTime;
|
|
248
|
-
}
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
private handleSeekComplete = (event: any) => {
|
|
252
|
-
if (this.seekStartPosition !== null) {
|
|
253
|
-
const seekDirection =
|
|
254
|
-
event.currentTime > this.seekStartPosition
|
|
255
|
-
? SEEK_DIRECTION.FORWARD
|
|
256
|
-
: SEEK_DIRECTION.REWIND;
|
|
257
|
-
|
|
258
|
-
const seekAmount = Math.round(
|
|
259
|
-
Math.abs(event.currentTime - this.seekStartPosition)
|
|
260
|
-
);
|
|
261
|
-
|
|
262
|
-
log_debug("Seek completed", {
|
|
263
|
-
seekDirection,
|
|
264
|
-
seekAmount,
|
|
265
|
-
fromPosition: this.seekStartPosition,
|
|
266
|
-
toPosition: event.currentTime,
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
this.accessibilityManager.readText({
|
|
270
|
-
text: `Skipped ${seekDirection} ${this.secondsToTime(seekAmount, "standard")}`,
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
this.seekStartPosition = event.currentTime;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
this.isSeeking = false;
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
private handleSeekStart = () => {
|
|
280
|
-
log_debug("Seek started");
|
|
281
|
-
this.isSeeking = true;
|
|
282
|
-
};
|
|
283
|
-
|
|
284
|
-
private handlePlayerClose = () => {
|
|
285
|
-
log_debug("Player closed - resetting state");
|
|
286
|
-
this.isInitialPlayerOpen = true;
|
|
287
|
-
this.accessibilityManager.resetInitialPlayerAnnouncementReady();
|
|
288
|
-
};
|
|
289
|
-
|
|
290
|
-
private announceAdBegin = (event: any) => {
|
|
291
|
-
this.isPrerollActive = true;
|
|
292
|
-
|
|
293
|
-
log_debug("Ad started", {
|
|
294
|
-
adDuration: event?.ad?.data?.duration,
|
|
295
|
-
isPrerollActive: this.isPrerollActive,
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
if (event?.ad?.data?.duration) {
|
|
299
|
-
this.accessibilityManager.readText({
|
|
300
|
-
text: `Sponsored. Ends in ${this.secondsToTime(event.ad.data.duration, "standard")}`,
|
|
301
|
-
});
|
|
302
|
-
}
|
|
303
|
-
};
|
|
304
|
-
|
|
305
|
-
private handleAdEnd = (_event: any) => {
|
|
306
|
-
this.isPrerollActive = false;
|
|
307
|
-
|
|
308
|
-
log_debug("Ad ended", {
|
|
309
|
-
isPrerollActive: this.isPrerollActive,
|
|
310
|
-
isInitialPlayerOpen: this.isInitialPlayerOpen,
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
// If initial entry still pending, trigger content announcement using latest player state
|
|
314
|
-
if (this.isInitialPlayerOpen) {
|
|
315
|
-
this.announceContentStart({ useReadText: true });
|
|
316
|
-
}
|
|
317
|
-
};
|
|
318
|
-
|
|
319
|
-
public init(): () => void {
|
|
320
|
-
if (!this.player) {
|
|
321
|
-
log_error("Failed to initialize PlayerTTS - no player provided");
|
|
322
|
-
|
|
323
|
-
return () => {};
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
log_debug("Initializing PlayerTTS listeners", {
|
|
327
|
-
listenerId: this.listenerId,
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
return this.player.addListener({
|
|
331
|
-
id: this.listenerId,
|
|
332
|
-
listener: {
|
|
333
|
-
onBufferComplete: this.announceBufferComplete,
|
|
334
|
-
onPlayerResume: this.announceResume,
|
|
335
|
-
onPlayerPause: this.announcePause,
|
|
336
|
-
onVideoProgress: this.handleVideoProgress,
|
|
337
|
-
onPlayerSeekStart: this.handleSeekStart,
|
|
338
|
-
onPlayerSeekComplete: this.handleSeekComplete,
|
|
339
|
-
onPlayerClose: this.handlePlayerClose,
|
|
340
|
-
onAdBegin: this.announceAdBegin,
|
|
341
|
-
onAdEnd: this.handleAdEnd,
|
|
342
|
-
onAdBreakEnd: this.handleAdEnd,
|
|
343
|
-
},
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
public destroy(): void {
|
|
348
|
-
log_debug("Destroying PlayerTTS", {
|
|
349
|
-
listenerId: this.listenerId,
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
if (this.player) {
|
|
353
|
-
this.player.removeListener(this.listenerId);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
this.seekStartPosition = null;
|
|
357
|
-
this.handlePlayerClose();
|
|
358
|
-
}
|
|
359
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { PlayerTTS } from "./PlayerTTS";
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import { usePlayer } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayer";
|
|
3
|
-
import { useAccessibilityManager } from "@applicaster/zapp-react-native-utils/appUtils/accessibilityManager/hooks";
|
|
4
|
-
import { PlayerTTS } from "@applicaster/zapp-react-native-utils/playerUtils/PlayerTTS";
|
|
5
|
-
|
|
6
|
-
export const usePlayerTTS = () => {
|
|
7
|
-
const player = usePlayer();
|
|
8
|
-
const accessibilityManager = useAccessibilityManager({});
|
|
9
|
-
|
|
10
|
-
React.useEffect(() => {
|
|
11
|
-
if (player && accessibilityManager) {
|
|
12
|
-
const playerTTS = new PlayerTTS(player, accessibilityManager);
|
|
13
|
-
const unsubscribe = playerTTS.init();
|
|
14
|
-
|
|
15
|
-
return () => {
|
|
16
|
-
unsubscribe();
|
|
17
|
-
playerTTS.destroy();
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
}, [player, accessibilityManager]);
|
|
21
|
-
};
|