@applicaster/zapp-react-native-utils 13.0.6 → 13.0.7-alpha.1457252144
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 +13 -0
- package/appUtils/accessibilityManager/hooks.ts +35 -1
- package/appUtils/accessibilityManager/index.ts +147 -16
- package/appUtils/platform/platformUtils.ts +32 -1
- package/index.d.ts +3 -0
- package/package.json +2 -2
- package/playerUtils/PlayerTTS/PlayerTTS.ts +230 -0
- package/playerUtils/PlayerTTS/index.ts +1 -0
- package/playerUtils/usePlayerTTS.ts +21 -0
|
@@ -65,4 +65,17 @@ 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
|
+
},
|
|
68
81
|
} as const;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useMemo } from "react";
|
|
1
|
+
import { useEffect, useMemo, useState } from "react";
|
|
2
2
|
import { AccessibilityManager } from "./index";
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -38,3 +38,37 @@ 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,3 +1,4 @@
|
|
|
1
|
+
import * as R from "ramda";
|
|
1
2
|
import { BehaviorSubject } from "rxjs";
|
|
2
3
|
import { accessibilityManagerLogger as logger } from "./logger";
|
|
3
4
|
import { TTSManager } from "../platform/platformUtils";
|
|
@@ -8,8 +9,10 @@ import _ from "lodash";
|
|
|
8
9
|
export class AccessibilityManager {
|
|
9
10
|
private static _instance: AccessibilityManager | null = null;
|
|
10
11
|
private headingTimeout: NodeJS.Timeout | null = null;
|
|
12
|
+
private announcementDelayTimeout: NodeJS.Timeout | null = null;
|
|
11
13
|
private WORDS_PER_MINUTE = 160;
|
|
12
14
|
private MINIMUM_PAUSE = 500;
|
|
15
|
+
private ANNOUNCEMENT_DELAY = 500;
|
|
13
16
|
private state$ = new BehaviorSubject<AccessibilityState>({
|
|
14
17
|
screenReaderEnabled: false,
|
|
15
18
|
reduceMotionEnabled: false,
|
|
@@ -25,6 +28,12 @@ export class AccessibilityManager {
|
|
|
25
28
|
private ttsManager = TTSManager.getInstance();
|
|
26
29
|
private localizations: { [key: string]: string } = {};
|
|
27
30
|
private headingQueue: string[] = [];
|
|
31
|
+
private currentFocusId: string | null = null;
|
|
32
|
+
private headingFocusMap: Map<string, string> = new Map();
|
|
33
|
+
private pendingFocusId: string | null = null;
|
|
34
|
+
private isInitialPlayerAnnouncementReady$ = new BehaviorSubject<boolean>(
|
|
35
|
+
false
|
|
36
|
+
);
|
|
28
37
|
|
|
29
38
|
private constructor() {}
|
|
30
39
|
|
|
@@ -36,6 +45,26 @@ export class AccessibilityManager {
|
|
|
36
45
|
return AccessibilityManager._instance;
|
|
37
46
|
}
|
|
38
47
|
|
|
48
|
+
public get isInitialPlayerAnnouncementReady(): boolean {
|
|
49
|
+
return this.isInitialPlayerAnnouncementReady$.getValue();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public setInitialPlayerAnnouncementReady(): void {
|
|
53
|
+
this.isInitialPlayerAnnouncementReady$.next(true);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public resetInitialPlayerAnnouncementReady(): void {
|
|
57
|
+
this.isInitialPlayerAnnouncementReady$.next(false);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public getInitialAnnouncementReadyObservable() {
|
|
61
|
+
return this.isInitialPlayerAnnouncementReady$.asObservable();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public getTTSStateObservable() {
|
|
65
|
+
return this.ttsManager.getStateAsObservable();
|
|
66
|
+
}
|
|
67
|
+
|
|
39
68
|
/**
|
|
40
69
|
* The method now accepts any object with localizations using a flattened structure
|
|
41
70
|
*
|
|
@@ -46,7 +75,9 @@ export class AccessibilityManager {
|
|
|
46
75
|
* i.e. localizations: [{ en: { accessibility_close_label: "Close", accessibility_close_hint: "Press here to close" } }]
|
|
47
76
|
*/
|
|
48
77
|
public updateLocalizations(localizations: { [key: string]: string }) {
|
|
49
|
-
|
|
78
|
+
if (!R.isEmpty(localizations)) {
|
|
79
|
+
this.localizations = localizations;
|
|
80
|
+
}
|
|
50
81
|
}
|
|
51
82
|
|
|
52
83
|
public getState(): AccessibilityState {
|
|
@@ -65,9 +96,11 @@ export class AccessibilityManager {
|
|
|
65
96
|
private calculateReadingTime(text: string): number {
|
|
66
97
|
const words = text.trim().split(/\s+/).length;
|
|
67
98
|
|
|
68
|
-
return
|
|
69
|
-
|
|
70
|
-
|
|
99
|
+
return (
|
|
100
|
+
Math.max(
|
|
101
|
+
this.MINIMUM_PAUSE,
|
|
102
|
+
(words / this.WORDS_PER_MINUTE) * 60 * 1000
|
|
103
|
+
) + 300
|
|
71
104
|
);
|
|
72
105
|
}
|
|
73
106
|
|
|
@@ -76,12 +109,20 @@ export class AccessibilityManager {
|
|
|
76
109
|
* Each heading will be read once and removed from the queue
|
|
77
110
|
*/
|
|
78
111
|
public addHeading(heading: string) {
|
|
112
|
+
if (!this.pendingFocusId) {
|
|
113
|
+
this.pendingFocusId = Date.now().toString();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
this.headingFocusMap.set(heading, this.pendingFocusId);
|
|
79
117
|
this.headingQueue.push(heading);
|
|
80
118
|
}
|
|
81
119
|
|
|
82
120
|
/**
|
|
83
121
|
* text you want to be read, if you want to use localized text pass keyOfLocalizedText instead
|
|
84
122
|
* keyOfLocalizedText is the key to the localized text
|
|
123
|
+
*
|
|
124
|
+
* Implements a delay mechanism to reduce noise during rapid navigation.
|
|
125
|
+
* Only the most recent announcement will be read after the delay period.
|
|
85
126
|
*/
|
|
86
127
|
public readText({
|
|
87
128
|
text,
|
|
@@ -112,19 +153,27 @@ export class AccessibilityManager {
|
|
|
112
153
|
textToRead = localizedMessage;
|
|
113
154
|
}
|
|
114
155
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
156
|
+
const focusId = this.pendingFocusId || Date.now().toString();
|
|
157
|
+
this.currentFocusId = focusId;
|
|
158
|
+
this.pendingFocusId = null;
|
|
118
159
|
|
|
119
|
-
|
|
120
|
-
clearTimeout(this.headingTimeout);
|
|
121
|
-
}
|
|
160
|
+
this.clearAnnouncement();
|
|
122
161
|
|
|
123
|
-
|
|
162
|
+
this.announcementDelayTimeout = setTimeout(() => {
|
|
163
|
+
this.executeAnnouncement(textToRead, keyOfLocalizedText, focusId);
|
|
164
|
+
}, this.ANNOUNCEMENT_DELAY);
|
|
165
|
+
}
|
|
124
166
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
167
|
+
/**
|
|
168
|
+
* Internal method to execute the actual announcement after the delay
|
|
169
|
+
*/
|
|
170
|
+
private executeAnnouncement(
|
|
171
|
+
textToRead: string,
|
|
172
|
+
keyOfLocalizedText?: string,
|
|
173
|
+
focusId?: string
|
|
174
|
+
) {
|
|
175
|
+
if (this.headingQueue.length > 0) {
|
|
176
|
+
this.processHeadingQueue(textToRead, focusId);
|
|
128
177
|
} else {
|
|
129
178
|
this.ttsManager?.readText(textToRead);
|
|
130
179
|
}
|
|
@@ -136,6 +185,49 @@ export class AccessibilityManager {
|
|
|
136
185
|
});
|
|
137
186
|
}
|
|
138
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Recursively processes all headings in the queue, reading them one by one
|
|
190
|
+
*/
|
|
191
|
+
private processHeadingQueue(textToRead: string, focusId?: string) {
|
|
192
|
+
// If focus has changed, abort this announcement
|
|
193
|
+
if (focusId && this.currentFocusId !== focusId) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (this.headingQueue.length === 0) {
|
|
198
|
+
if (focusId && this.currentFocusId === focusId) {
|
|
199
|
+
this.ttsManager?.readText(textToRead);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const heading = this.headingQueue.shift()!;
|
|
206
|
+
|
|
207
|
+
const headingFocusId = this.headingFocusMap.get(heading);
|
|
208
|
+
|
|
209
|
+
if (headingFocusId && headingFocusId !== focusId) {
|
|
210
|
+
// This heading belongs to a previous focus, skip it
|
|
211
|
+
this.headingFocusMap.delete(heading);
|
|
212
|
+
this.processHeadingQueue(textToRead, focusId);
|
|
213
|
+
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
this.ttsManager?.readText(heading);
|
|
218
|
+
this.headingFocusMap.delete(heading); // Clean up after reading
|
|
219
|
+
|
|
220
|
+
if (this.headingTimeout) {
|
|
221
|
+
clearTimeout(this.headingTimeout);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const pauseTime = this.calculateReadingTime(heading);
|
|
225
|
+
|
|
226
|
+
this.headingTimeout = setTimeout(() => {
|
|
227
|
+
this.processHeadingQueue(textToRead, focusId);
|
|
228
|
+
}, pauseTime);
|
|
229
|
+
}
|
|
230
|
+
|
|
139
231
|
public getButtonAccessibilityProps(name: string): AccessibilityProps {
|
|
140
232
|
const buttonName = _.toString(name);
|
|
141
233
|
|
|
@@ -143,12 +235,15 @@ export class AccessibilityManager {
|
|
|
143
235
|
|
|
144
236
|
if (!buttonConfig) {
|
|
145
237
|
return {
|
|
238
|
+
accessible: true,
|
|
146
239
|
accessibilityLabel: buttonName,
|
|
147
240
|
accessibilityHint: `Press button to perform action on ${buttonName}`,
|
|
148
241
|
"aria-label": buttonName,
|
|
149
242
|
"aria-description": `Press button to perform action on ${buttonName}`,
|
|
150
243
|
accessibilityRole: "button" as AccessibilityRole,
|
|
151
244
|
"aria-role": "button",
|
|
245
|
+
role: "button",
|
|
246
|
+
tabindex: 0,
|
|
152
247
|
};
|
|
153
248
|
}
|
|
154
249
|
|
|
@@ -162,23 +257,52 @@ export class AccessibilityManager {
|
|
|
162
257
|
`Press button to perform action on ${buttonName}`;
|
|
163
258
|
|
|
164
259
|
return {
|
|
260
|
+
accessible: true,
|
|
165
261
|
accessibilityLabel: label,
|
|
166
262
|
accessibilityHint: hint,
|
|
167
263
|
"aria-label": label,
|
|
168
264
|
"aria-description": hint,
|
|
169
265
|
accessibilityRole: "button" as AccessibilityRole,
|
|
170
266
|
"aria-role": "button",
|
|
267
|
+
role: "button",
|
|
268
|
+
tabindex: 0,
|
|
171
269
|
};
|
|
172
270
|
}
|
|
173
271
|
|
|
174
272
|
public getInputAccessibilityProps(inputName: string): AccessibilityProps {
|
|
175
273
|
return {
|
|
274
|
+
accessible: true,
|
|
176
275
|
accessibilityLabel: inputName,
|
|
177
276
|
accessibilityHint: `Enter text into ${inputName}`,
|
|
178
277
|
"aria-label": inputName,
|
|
179
278
|
"aria-description": `Enter text into ${inputName}`,
|
|
180
|
-
accessibilityRole: "
|
|
181
|
-
"aria-role": "
|
|
279
|
+
accessibilityRole: "text" as AccessibilityRole,
|
|
280
|
+
"aria-role": "text",
|
|
281
|
+
role: "text",
|
|
282
|
+
tabindex: 0,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Extracts accessibility props from component props and returns them as HTML attributes
|
|
288
|
+
* @param props - Component props containing accessibility properties
|
|
289
|
+
* @returns Object with accessibility HTML attributes
|
|
290
|
+
*/
|
|
291
|
+
public getWebAccessibilityProps(props: any): AccessibilityProps {
|
|
292
|
+
const {
|
|
293
|
+
"aria-label": ariaLabel,
|
|
294
|
+
"aria-description": ariaDescription,
|
|
295
|
+
"aria-role": ariaRole,
|
|
296
|
+
role,
|
|
297
|
+
tabindex,
|
|
298
|
+
} = props;
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
"aria-label": ariaLabel,
|
|
302
|
+
"aria-description": ariaDescription,
|
|
303
|
+
"aria-role": ariaRole,
|
|
304
|
+
role: role || ariaRole,
|
|
305
|
+
tabindex,
|
|
182
306
|
};
|
|
183
307
|
}
|
|
184
308
|
|
|
@@ -196,4 +320,11 @@ export class AccessibilityManager {
|
|
|
196
320
|
|
|
197
321
|
return this.localizations[key];
|
|
198
322
|
}
|
|
323
|
+
|
|
324
|
+
private clearAnnouncement() {
|
|
325
|
+
if (this.announcementDelayTimeout) {
|
|
326
|
+
clearTimeout(this.announcementDelayTimeout);
|
|
327
|
+
this.announcementDelayTimeout = null;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
199
330
|
}
|
|
@@ -212,9 +212,16 @@ export class TTSManager {
|
|
|
212
212
|
}
|
|
213
213
|
|
|
214
214
|
readText(text: string) {
|
|
215
|
+
this.ttsState$.next(true);
|
|
216
|
+
|
|
215
217
|
if (isSamsungPlatform() && window.speechSynthesis) {
|
|
216
218
|
const utterance = new SpeechSynthesisUtterance(text);
|
|
219
|
+
|
|
220
|
+
window.speechSynthesis.cancel(); // Cancel previous speech before speaking new text
|
|
217
221
|
window.speechSynthesis.speak(utterance);
|
|
222
|
+
|
|
223
|
+
// Estimate reading time and set inactive when done
|
|
224
|
+
this.scheduleTTSComplete(text);
|
|
218
225
|
}
|
|
219
226
|
|
|
220
227
|
if (isLgPlatform() && window.webOS?.service) {
|
|
@@ -225,23 +232,47 @@ export class TTSManager {
|
|
|
225
232
|
log_debug("There was a failure setting up webOS TTS service", {
|
|
226
233
|
error,
|
|
227
234
|
});
|
|
235
|
+
|
|
236
|
+
this.ttsState$.next(false);
|
|
228
237
|
},
|
|
229
238
|
onSuccess(response: any) {
|
|
230
239
|
log_debug("webOS TTS service is configured successfully", {
|
|
231
240
|
response,
|
|
232
241
|
});
|
|
242
|
+
|
|
243
|
+
// Estimate reading time and set inactive when done
|
|
244
|
+
this.scheduleTTSComplete(text);
|
|
233
245
|
},
|
|
234
246
|
parameters: {
|
|
235
247
|
text,
|
|
248
|
+
clear: true, // Clear any previous speech before speaking new text
|
|
236
249
|
},
|
|
237
250
|
});
|
|
238
251
|
} catch (error) {
|
|
239
252
|
log_debug("webOS TTS service error", { error });
|
|
253
|
+
this.ttsState$.next(false);
|
|
240
254
|
}
|
|
241
255
|
}
|
|
242
256
|
|
|
243
|
-
if (!window.VIZIO?.Chromevox)
|
|
257
|
+
if (!window.VIZIO?.Chromevox) {
|
|
258
|
+
// For platforms without TTS, estimate reading time
|
|
259
|
+
this.scheduleTTSComplete(text);
|
|
260
|
+
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
244
263
|
|
|
245
264
|
window.VIZIO.Chromevox.play(text);
|
|
265
|
+
// Estimate reading time and set inactive when done
|
|
266
|
+
this.scheduleTTSComplete(text);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private scheduleTTSComplete(text: string) {
|
|
270
|
+
// Estimate reading time based on word count (160 words per minute)
|
|
271
|
+
const words = text.trim().split(/\s+/).length;
|
|
272
|
+
const readingTime = Math.max(500, (words / 160) * 60 * 1000) + 300;
|
|
273
|
+
|
|
274
|
+
setTimeout(() => {
|
|
275
|
+
this.ttsState$.next(false);
|
|
276
|
+
}, readingTime);
|
|
246
277
|
}
|
|
247
278
|
}
|
package/index.d.ts
CHANGED
|
@@ -139,10 +139,13 @@ declare type AccessibilityState = {
|
|
|
139
139
|
};
|
|
140
140
|
|
|
141
141
|
declare type AccessibilityProps = {
|
|
142
|
+
accessible?: boolean;
|
|
142
143
|
accessibilityLabel?: string;
|
|
143
144
|
accessibilityHint?: string;
|
|
144
145
|
"aria-label"?: string;
|
|
145
146
|
"aria-description"?: string;
|
|
146
147
|
accessibilityRole?: string;
|
|
147
148
|
"aria-role"?: string;
|
|
149
|
+
role?: string;
|
|
150
|
+
tabindex?: number;
|
|
148
151
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applicaster/zapp-react-native-utils",
|
|
3
|
-
"version": "13.0.
|
|
3
|
+
"version": "13.0.7-alpha.1457252144",
|
|
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.
|
|
30
|
+
"@applicaster/applicaster-types": "13.0.7-alpha.1457252144",
|
|
31
31
|
"buffer": "^5.2.1",
|
|
32
32
|
"camelize": "^1.0.0",
|
|
33
33
|
"dayjs": "^1.11.10",
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { AccessibilityManager } from "@applicaster/zapp-react-native-utils/appUtils/accessibilityManager";
|
|
2
|
+
import uuidv4 from "uuid/v4";
|
|
3
|
+
import { Player } from "../../appUtils/playerManager/player";
|
|
4
|
+
|
|
5
|
+
enum SEEK_DIRECTION {
|
|
6
|
+
FORWARD = "forward",
|
|
7
|
+
REWIND = "back",
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class PlayerTTS {
|
|
11
|
+
private player: Player;
|
|
12
|
+
private accessibilityManager: AccessibilityManager;
|
|
13
|
+
private seekStartPosition: number | null = null;
|
|
14
|
+
private isSeeking: boolean = false;
|
|
15
|
+
private listenerId: string;
|
|
16
|
+
private isInitialPlayerOpen: boolean = true;
|
|
17
|
+
|
|
18
|
+
constructor(player: Player, accessibilityManager: AccessibilityManager) {
|
|
19
|
+
this.player = player;
|
|
20
|
+
this.accessibilityManager = accessibilityManager;
|
|
21
|
+
this.listenerId = `player-tts-${uuidv4()}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private numberToWords(num: number): string {
|
|
25
|
+
const ones = [
|
|
26
|
+
"",
|
|
27
|
+
"one",
|
|
28
|
+
"two",
|
|
29
|
+
"three",
|
|
30
|
+
"four",
|
|
31
|
+
"five",
|
|
32
|
+
"six",
|
|
33
|
+
"seven",
|
|
34
|
+
"eight",
|
|
35
|
+
"nine",
|
|
36
|
+
"ten",
|
|
37
|
+
"eleven",
|
|
38
|
+
"twelve",
|
|
39
|
+
"thirteen",
|
|
40
|
+
"fourteen",
|
|
41
|
+
"fifteen",
|
|
42
|
+
"sixteen",
|
|
43
|
+
"seventeen",
|
|
44
|
+
"eighteen",
|
|
45
|
+
"nineteen",
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const tens = [
|
|
49
|
+
"",
|
|
50
|
+
"",
|
|
51
|
+
"twenty",
|
|
52
|
+
"thirty",
|
|
53
|
+
"forty",
|
|
54
|
+
"fifty",
|
|
55
|
+
"sixty",
|
|
56
|
+
"seventy",
|
|
57
|
+
"eighty",
|
|
58
|
+
"ninety",
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
if (num === 0) return "zero";
|
|
62
|
+
if (num < 20) return ones[num];
|
|
63
|
+
|
|
64
|
+
const ten = Math.floor(num / 10);
|
|
65
|
+
const one = num % 10;
|
|
66
|
+
|
|
67
|
+
if (one === 0) return tens[ten];
|
|
68
|
+
|
|
69
|
+
return `${tens[ten]} ${ones[one]}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private secondsToTime(
|
|
73
|
+
seconds: number,
|
|
74
|
+
format: "natural" | "standard" = "natural"
|
|
75
|
+
): string {
|
|
76
|
+
if (seconds < 0) return format === "natural" ? "zero" : "0";
|
|
77
|
+
|
|
78
|
+
const minutes = Math.floor(seconds / 60);
|
|
79
|
+
const remainingSeconds = Math.floor(seconds % 60);
|
|
80
|
+
|
|
81
|
+
if (format === "standard") {
|
|
82
|
+
if (minutes === 0) {
|
|
83
|
+
if (remainingSeconds === 0) return "0";
|
|
84
|
+
|
|
85
|
+
return `${remainingSeconds} second${remainingSeconds !== 1 ? "s" : ""}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (remainingSeconds === 0) {
|
|
89
|
+
return `${minutes} minute${minutes !== 1 ? "s" : ""}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return `${minutes} minute${minutes !== 1 ? "s" : ""} ${remainingSeconds} second${remainingSeconds !== 1 ? "s" : ""}`;
|
|
93
|
+
} else {
|
|
94
|
+
if (minutes === 0) {
|
|
95
|
+
if (remainingSeconds === 0) return "zero";
|
|
96
|
+
|
|
97
|
+
if (remainingSeconds < 10) {
|
|
98
|
+
return `zero o ${this.numberToWords(remainingSeconds)}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return `zero ${this.numberToWords(remainingSeconds)}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (remainingSeconds === 0) {
|
|
105
|
+
return `${this.numberToWords(minutes)}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (remainingSeconds < 10) {
|
|
109
|
+
return `${this.numberToWords(minutes)} o ${this.numberToWords(remainingSeconds)}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return `${this.numberToWords(minutes)} ${this.numberToWords(remainingSeconds)}`;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private announceLoading = () => {
|
|
117
|
+
this.accessibilityManager.addHeading("Loading");
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
private announcePause = () => {
|
|
121
|
+
if (!this.isSeeking) {
|
|
122
|
+
this.accessibilityManager.addHeading(
|
|
123
|
+
`Paused - ${this.secondsToTime(this.player.playerState.contentPosition, "standard")}`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
private announceBufferComplete = (event: any) => {
|
|
129
|
+
if (this.isInitialPlayerOpen) {
|
|
130
|
+
const timeRemaining = event.duration - event.currentTime;
|
|
131
|
+
const title = (this.player.entry.title as string) || "";
|
|
132
|
+
const summary = (this.player.entry.summary as string) || "";
|
|
133
|
+
this.accessibilityManager.addHeading(`Playing - ${title}`);
|
|
134
|
+
this.accessibilityManager.addHeading(summary);
|
|
135
|
+
|
|
136
|
+
this.accessibilityManager.addHeading(
|
|
137
|
+
`Playing from ${this.secondsToTime(event.currentTime, "standard")}`
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
this.accessibilityManager.addHeading(
|
|
141
|
+
`${this.secondsToTime(timeRemaining, "standard")} remaining.`
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
this.accessibilityManager.setInitialPlayerAnnouncementReady();
|
|
145
|
+
this.isInitialPlayerOpen = false;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
private announceResume = () => {
|
|
150
|
+
if (!this.isSeeking && !this.isInitialPlayerOpen) {
|
|
151
|
+
this.accessibilityManager.addHeading(
|
|
152
|
+
`Playing - ${this.secondsToTime(this.player.playerState.contentPosition, "standard")}`
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
private handleVideoProgress = (event: any) => {
|
|
158
|
+
if (event.currentTime > 0) {
|
|
159
|
+
this.seekStartPosition = event.currentTime;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
private handleSeekComplete = (event: any) => {
|
|
164
|
+
if (this.seekStartPosition !== null) {
|
|
165
|
+
const seekDirection =
|
|
166
|
+
event.currentTime > this.seekStartPosition
|
|
167
|
+
? SEEK_DIRECTION.FORWARD
|
|
168
|
+
: SEEK_DIRECTION.REWIND;
|
|
169
|
+
|
|
170
|
+
const seekAmount = Math.round(
|
|
171
|
+
Math.abs(event.currentTime - this.seekStartPosition)
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
this.accessibilityManager.readText({
|
|
175
|
+
text: `Skipped ${seekDirection} ${this.secondsToTime(seekAmount, "standard")}`,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
this.seekStartPosition = event.currentTime;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
this.isSeeking = false;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
private handleSeekStart = () => {
|
|
185
|
+
this.isSeeking = true;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
private handlePlayerClose = () => {
|
|
189
|
+
this.isInitialPlayerOpen = true;
|
|
190
|
+
this.accessibilityManager.resetInitialPlayerAnnouncementReady();
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
private announceAdBegin = (event: any) => {
|
|
194
|
+
if (event?.ad?.data?.duration) {
|
|
195
|
+
this.accessibilityManager.readText({
|
|
196
|
+
text: `Sponsored. Ends in ${this.secondsToTime(event.ad.data.duration, "standard")}`,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
public init(): () => void {
|
|
202
|
+
if (!this.player) {
|
|
203
|
+
return () => {};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return this.player.addListener({
|
|
207
|
+
id: this.listenerId,
|
|
208
|
+
listener: {
|
|
209
|
+
onBufferStart: this.announceLoading,
|
|
210
|
+
onBufferComplete: this.announceBufferComplete,
|
|
211
|
+
onPlayerResume: this.announceResume,
|
|
212
|
+
onPlayerPause: this.announcePause,
|
|
213
|
+
onVideoProgress: this.handleVideoProgress,
|
|
214
|
+
onPlayerSeekStart: this.handleSeekStart,
|
|
215
|
+
onPlayerSeekComplete: this.handleSeekComplete,
|
|
216
|
+
onPlayerClose: this.handlePlayerClose,
|
|
217
|
+
onAdBegin: this.announceAdBegin,
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
public destroy(): void {
|
|
223
|
+
if (this.player) {
|
|
224
|
+
this.player.removeListener(this.listenerId);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
this.seekStartPosition = null;
|
|
228
|
+
this.handlePlayerClose();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { PlayerTTS } from "./PlayerTTS";
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
};
|