vidply 1.0.5 → 1.0.7

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,248 +1,278 @@
1
- /**
2
- * Caption/Subtitle Manager
3
- */
4
-
5
- import {DOMUtils} from '../utils/DOMUtils.js';
6
- import {i18n} from '../i18n/i18n.js';
7
-
8
- export class CaptionManager {
9
- constructor(player) {
10
- this.player = player;
11
- this.element = null;
12
- this.tracks = [];
13
- this.currentTrack = null;
14
- this.currentCue = null;
15
-
16
- this.init();
17
- }
18
-
19
- init() {
20
- this.createElement();
21
- this.loadTracks();
22
- this.attachEvents();
23
-
24
- if (this.player.options.captionsDefault && this.tracks.length > 0) {
25
- this.enable();
26
- }
27
- }
28
-
29
- createElement() {
30
- this.element = DOMUtils.createElement('div', {
31
- className: `${this.player.options.classPrefix}-captions`,
32
- attributes: {
33
- 'aria-live': 'polite',
34
- 'aria-atomic': 'true',
35
- 'role': 'region',
36
- 'aria-label': i18n.t('player.captions')
37
- }
38
- });
39
-
40
- // Apply caption styles
41
- this.updateStyles();
42
-
43
- // Append to videoWrapper if it exists, otherwise to container
44
- const target = this.player.videoWrapper || this.player.container;
45
- target.appendChild(this.element);
46
- }
47
-
48
- loadTracks() {
49
- const textTracks = this.player.element.textTracks;
50
-
51
- for (let i = 0; i < textTracks.length; i++) {
52
- const track = textTracks[i];
53
-
54
- if (track.kind === 'subtitles' || track.kind === 'captions') {
55
- this.tracks.push({
56
- track: track,
57
- language: track.language,
58
- label: track.label,
59
- kind: track.kind,
60
- index: i
61
- });
62
-
63
- // Disable all tracks initially
64
- track.mode = 'hidden';
65
- }
66
- }
67
- }
68
-
69
- attachEvents() {
70
- this.player.on('timeupdate', () => {
71
- this.updateCaptions();
72
- });
73
-
74
- this.player.on('captionschange', () => {
75
- this.updateStyles();
76
- });
77
- }
78
-
79
- enable(trackIndex = 0) {
80
- if (this.tracks.length === 0) {
81
- return;
82
- }
83
-
84
- // Disable current track
85
- if (this.currentTrack) {
86
- this.currentTrack.track.mode = 'hidden';
87
- }
88
-
89
- // Enable selected track
90
- const selectedTrack = this.tracks[trackIndex];
91
-
92
- if (selectedTrack) {
93
- // Set to 'hidden' not 'showing' to prevent browser from displaying native captions
94
- // We'll handle the display ourselves
95
- selectedTrack.track.mode = 'hidden';
96
- this.currentTrack = selectedTrack;
97
- this.player.state.captionsEnabled = true;
98
-
99
- // Remove any existing cuechange listener
100
- if (this.cueChangeHandler) {
101
- selectedTrack.track.removeEventListener('cuechange', this.cueChangeHandler);
102
- }
103
-
104
- // Add event listener for cue changes
105
- this.cueChangeHandler = () => {
106
- this.updateCaptions();
107
- };
108
- selectedTrack.track.addEventListener('cuechange', this.cueChangeHandler);
109
-
110
- this.player.emit('captionsenabled', selectedTrack);
111
- }
112
- }
113
-
114
- disable() {
115
- if (this.currentTrack) {
116
- this.currentTrack.track.mode = 'hidden';
117
- this.currentTrack = null;
118
- }
119
-
120
- this.element.style.display = 'none';
121
- this.element.innerHTML = '';
122
- this.currentCue = null;
123
- this.player.state.captionsEnabled = false;
124
- this.player.emit('captionsdisabled');
125
- }
126
-
127
- updateCaptions() {
128
- if (!this.currentTrack) {
129
- return;
130
- }
131
-
132
- if (!this.currentTrack.track.activeCues) {
133
- return;
134
- }
135
-
136
- const activeCues = this.currentTrack.track.activeCues;
137
-
138
- if (activeCues.length > 0) {
139
- const cue = activeCues[0];
140
-
141
- // Only update if the cue has changed
142
- if (this.currentCue !== cue) {
143
- this.currentCue = cue;
144
-
145
- // Parse and display cue text
146
- let text = cue.text;
147
-
148
- // Handle VTT formatting
149
- text = this.parseVTTFormatting(text);
150
-
151
- this.element.innerHTML = DOMUtils.sanitizeHTML(text);
152
-
153
- // Make sure it's visible when there's content
154
- this.element.style.display = 'block';
155
-
156
- this.player.emit('captionchange', cue);
157
- }
158
- } else if (this.currentCue) {
159
- // Clear caption
160
- this.element.innerHTML = '';
161
- this.element.style.display = 'none';
162
- this.currentCue = null;
163
- }
164
- }
165
-
166
- parseVTTFormatting(text) {
167
- // Basic VTT tag support
168
- text = text.replace(/<c[^>]*>(.*?)<\/c>/g, '<span class="caption-class">$1</span>');
169
- text = text.replace(/<b>(.*?)<\/b>/g, '<strong>$1</strong>');
170
- text = text.replace(/<i>(.*?)<\/i>/g, '<em>$1</em>');
171
- text = text.replace(/<u>(.*?)<\/u>/g, '<u>$1</u>');
172
-
173
- // Voice tags
174
- text = text.replace(/<v\s+([^>]+)>(.*?)<\/v>/g, '<span class="caption-voice" data-voice="$1">$2</span>');
175
-
176
- return text;
177
- }
178
-
179
- updateStyles() {
180
- if (!this.element) return;
181
-
182
- const options = this.player.options;
183
-
184
- this.element.style.fontSize = options.captionsFontSize;
185
- this.element.style.fontFamily = options.captionsFontFamily;
186
- this.element.style.color = options.captionsColor;
187
- this.element.style.backgroundColor = this.hexToRgba(
188
- options.captionsBackgroundColor,
189
- options.captionsOpacity
190
- );
191
- }
192
-
193
- hexToRgba(hex, alpha) {
194
- const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
195
- if (result) {
196
- return `rgba(${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)}, ${alpha})`;
197
- }
198
- return hex;
199
- }
200
-
201
- setCaptionStyle(property, value) {
202
- switch (property) {
203
- case 'fontSize':
204
- this.player.options.captionsFontSize = value;
205
- break;
206
- case 'fontFamily':
207
- this.player.options.captionsFontFamily = value;
208
- break;
209
- case 'color':
210
- this.player.options.captionsColor = value;
211
- break;
212
- case 'backgroundColor':
213
- this.player.options.captionsBackgroundColor = value;
214
- break;
215
- case 'opacity':
216
- this.player.options.captionsOpacity = value;
217
- break;
218
- }
219
-
220
- this.updateStyles();
221
- this.player.emit('captionschange');
222
- }
223
-
224
- getAvailableTracks() {
225
- return this.tracks.map((t, index) => ({
226
- index,
227
- language: t.language,
228
- label: t.label || t.language,
229
- kind: t.kind
230
- }));
231
- }
232
-
233
- switchTrack(trackIndex) {
234
- if (trackIndex >= 0 && trackIndex < this.tracks.length) {
235
- this.disable();
236
- this.enable(trackIndex);
237
- }
238
- }
239
-
240
- destroy() {
241
- this.disable();
242
-
243
- if (this.element && this.element.parentNode) {
244
- this.element.parentNode.removeChild(this.element);
245
- }
246
- }
247
- }
248
-
1
+ /**
2
+ * Caption/Subtitle Manager
3
+ */
4
+
5
+ import {DOMUtils} from '../utils/DOMUtils.js';
6
+ import {i18n} from '../i18n/i18n.js';
7
+ import {StorageManager} from '../utils/StorageManager.js';
8
+
9
+ export class CaptionManager {
10
+ constructor(player) {
11
+ this.player = player;
12
+ this.element = null;
13
+ this.tracks = [];
14
+ this.currentTrack = null;
15
+ this.currentCue = null;
16
+
17
+ // Storage manager
18
+ this.storage = new StorageManager('vidply');
19
+
20
+ // Load saved preferences
21
+ this.loadSavedPreferences();
22
+
23
+ this.init();
24
+ }
25
+
26
+ loadSavedPreferences() {
27
+ const saved = this.storage.getCaptionPreferences();
28
+ if (saved) {
29
+ // Override player options with saved preferences
30
+ if (saved.fontSize) this.player.options.captionsFontSize = saved.fontSize;
31
+ if (saved.fontFamily) this.player.options.captionsFontFamily = saved.fontFamily;
32
+ if (saved.color) this.player.options.captionsColor = saved.color;
33
+ if (saved.backgroundColor) this.player.options.captionsBackgroundColor = saved.backgroundColor;
34
+ if (saved.opacity !== undefined) this.player.options.captionsOpacity = saved.opacity;
35
+ }
36
+ }
37
+
38
+ saveCaptionPreferences() {
39
+ this.storage.saveCaptionPreferences({
40
+ fontSize: this.player.options.captionsFontSize,
41
+ fontFamily: this.player.options.captionsFontFamily,
42
+ color: this.player.options.captionsColor,
43
+ backgroundColor: this.player.options.captionsBackgroundColor,
44
+ opacity: this.player.options.captionsOpacity
45
+ });
46
+ }
47
+
48
+ init() {
49
+ this.createElement();
50
+ this.loadTracks();
51
+ this.attachEvents();
52
+
53
+ if (this.player.options.captionsDefault && this.tracks.length > 0) {
54
+ this.enable();
55
+ }
56
+ }
57
+
58
+ createElement() {
59
+ this.element = DOMUtils.createElement('div', {
60
+ className: `${this.player.options.classPrefix}-captions`,
61
+ attributes: {
62
+ 'aria-live': 'polite',
63
+ 'aria-atomic': 'true',
64
+ 'role': 'region',
65
+ 'aria-label': i18n.t('player.captions')
66
+ }
67
+ });
68
+
69
+ // Apply caption styles
70
+ this.updateStyles();
71
+
72
+ // Append to videoWrapper if it exists, otherwise to container
73
+ const target = this.player.videoWrapper || this.player.container;
74
+ target.appendChild(this.element);
75
+ }
76
+
77
+ loadTracks() {
78
+ const textTracks = this.player.element.textTracks;
79
+
80
+ for (let i = 0; i < textTracks.length; i++) {
81
+ const track = textTracks[i];
82
+
83
+ if (track.kind === 'subtitles' || track.kind === 'captions') {
84
+ this.tracks.push({
85
+ track: track,
86
+ language: track.language,
87
+ label: track.label,
88
+ kind: track.kind,
89
+ index: i
90
+ });
91
+
92
+ // Disable all tracks initially
93
+ track.mode = 'hidden';
94
+ }
95
+ }
96
+ }
97
+
98
+ attachEvents() {
99
+ this.player.on('timeupdate', () => {
100
+ this.updateCaptions();
101
+ });
102
+
103
+ this.player.on('captionschange', () => {
104
+ this.updateStyles();
105
+ });
106
+ }
107
+
108
+ enable(trackIndex = 0) {
109
+ if (this.tracks.length === 0) {
110
+ return;
111
+ }
112
+
113
+ // Disable current track
114
+ if (this.currentTrack) {
115
+ this.currentTrack.track.mode = 'hidden';
116
+ }
117
+
118
+ // Enable selected track
119
+ const selectedTrack = this.tracks[trackIndex];
120
+
121
+ if (selectedTrack) {
122
+ // Set to 'hidden' not 'showing' to prevent browser from displaying native captions
123
+ // We'll handle the display ourselves
124
+ selectedTrack.track.mode = 'hidden';
125
+ this.currentTrack = selectedTrack;
126
+ this.player.state.captionsEnabled = true;
127
+
128
+ // Remove any existing cuechange listener
129
+ if (this.cueChangeHandler) {
130
+ selectedTrack.track.removeEventListener('cuechange', this.cueChangeHandler);
131
+ }
132
+
133
+ // Add event listener for cue changes
134
+ this.cueChangeHandler = () => {
135
+ this.updateCaptions();
136
+ };
137
+ selectedTrack.track.addEventListener('cuechange', this.cueChangeHandler);
138
+
139
+ this.player.emit('captionsenabled', selectedTrack);
140
+ }
141
+ }
142
+
143
+ disable() {
144
+ if (this.currentTrack) {
145
+ this.currentTrack.track.mode = 'hidden';
146
+ this.currentTrack = null;
147
+ }
148
+
149
+ this.element.style.display = 'none';
150
+ this.element.innerHTML = '';
151
+ this.currentCue = null;
152
+ this.player.state.captionsEnabled = false;
153
+ this.player.emit('captionsdisabled');
154
+ }
155
+
156
+ updateCaptions() {
157
+ if (!this.currentTrack) {
158
+ return;
159
+ }
160
+
161
+ if (!this.currentTrack.track.activeCues) {
162
+ return;
163
+ }
164
+
165
+ const activeCues = this.currentTrack.track.activeCues;
166
+
167
+ if (activeCues.length > 0) {
168
+ const cue = activeCues[0];
169
+
170
+ // Only update if the cue has changed
171
+ if (this.currentCue !== cue) {
172
+ this.currentCue = cue;
173
+
174
+ // Parse and display cue text
175
+ let text = cue.text;
176
+
177
+ // Handle VTT formatting
178
+ text = this.parseVTTFormatting(text);
179
+
180
+ this.element.innerHTML = DOMUtils.sanitizeHTML(text);
181
+
182
+ // Make sure it's visible when there's content
183
+ this.element.style.display = 'block';
184
+
185
+ this.player.emit('captionchange', cue);
186
+ }
187
+ } else if (this.currentCue) {
188
+ // Clear caption
189
+ this.element.innerHTML = '';
190
+ this.element.style.display = 'none';
191
+ this.currentCue = null;
192
+ }
193
+ }
194
+
195
+ parseVTTFormatting(text) {
196
+ // Basic VTT tag support
197
+ text = text.replace(/<c[^>]*>(.*?)<\/c>/g, '<span class="caption-class">$1</span>');
198
+ text = text.replace(/<b>(.*?)<\/b>/g, '<strong>$1</strong>');
199
+ text = text.replace(/<i>(.*?)<\/i>/g, '<em>$1</em>');
200
+ text = text.replace(/<u>(.*?)<\/u>/g, '<u>$1</u>');
201
+
202
+ // Voice tags
203
+ text = text.replace(/<v\s+([^>]+)>(.*?)<\/v>/g, '<span class="caption-voice" data-voice="$1">$2</span>');
204
+
205
+ return text;
206
+ }
207
+
208
+ updateStyles() {
209
+ if (!this.element) return;
210
+
211
+ const options = this.player.options;
212
+
213
+ this.element.style.fontSize = options.captionsFontSize;
214
+ this.element.style.fontFamily = options.captionsFontFamily;
215
+ this.element.style.color = options.captionsColor;
216
+ this.element.style.backgroundColor = this.hexToRgba(
217
+ options.captionsBackgroundColor,
218
+ options.captionsOpacity
219
+ );
220
+ }
221
+
222
+ hexToRgba(hex, alpha) {
223
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
224
+ if (result) {
225
+ return `rgba(${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)}, ${alpha})`;
226
+ }
227
+ return hex;
228
+ }
229
+
230
+ setCaptionStyle(property, value) {
231
+ switch (property) {
232
+ case 'fontSize':
233
+ this.player.options.captionsFontSize = value;
234
+ break;
235
+ case 'fontFamily':
236
+ this.player.options.captionsFontFamily = value;
237
+ break;
238
+ case 'color':
239
+ this.player.options.captionsColor = value;
240
+ break;
241
+ case 'backgroundColor':
242
+ this.player.options.captionsBackgroundColor = value;
243
+ break;
244
+ case 'opacity':
245
+ this.player.options.captionsOpacity = value;
246
+ break;
247
+ }
248
+
249
+ this.updateStyles();
250
+ this.saveCaptionPreferences();
251
+ this.player.emit('captionschange');
252
+ }
253
+
254
+ getAvailableTracks() {
255
+ return this.tracks.map((t, index) => ({
256
+ index,
257
+ language: t.language,
258
+ label: t.label || t.language,
259
+ kind: t.kind
260
+ }));
261
+ }
262
+
263
+ switchTrack(trackIndex) {
264
+ if (trackIndex >= 0 && trackIndex < this.tracks.length) {
265
+ this.disable();
266
+ this.enable(trackIndex);
267
+ }
268
+ }
269
+
270
+ destroy() {
271
+ this.disable();
272
+
273
+ if (this.element && this.element.parentNode) {
274
+ this.element.parentNode.removeChild(this.element);
275
+ }
276
+ }
277
+ }
278
+