vidply 1.0.4 → 1.0.6

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,248 @@
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
+
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
+
@@ -701,7 +701,7 @@ export class ControlBar {
701
701
  className: `${this.player.options.classPrefix}-time`,
702
702
  attributes: {
703
703
  'role': 'group',
704
- 'aria-label': 'Time display'
704
+ 'aria-label': i18n.t('time.display')
705
705
  }
706
706
  });
707
707
 
@@ -709,7 +709,7 @@ export class ControlBar {
709
709
  this.controls.currentTimeDisplay = DOMUtils.createElement('span', {
710
710
  className: `${this.player.options.classPrefix}-current-time`,
711
711
  attributes: {
712
- 'aria-label': '0 seconds'
712
+ 'aria-label': i18n.t('time.seconds', { count: 0 })
713
713
  }
714
714
  });
715
715
 
@@ -734,7 +734,7 @@ export class ControlBar {
734
734
  this.controls.durationDisplay = DOMUtils.createElement('span', {
735
735
  className: `${this.player.options.classPrefix}-duration`,
736
736
  attributes: {
737
- 'aria-label': 'Duration: 0 seconds'
737
+ 'aria-label': i18n.t('time.durationPrefix') + i18n.t('time.seconds', { count: 0 })
738
738
  }
739
739
  });
740
740
 
@@ -1835,7 +1835,7 @@ export class ControlBar {
1835
1835
  // Update visual text (hidden from screen readers)
1836
1836
  this.controls.durationVisual.textContent = TimeUtils.formatTime(duration);
1837
1837
  // Update aria-label with human-readable format
1838
- this.controls.durationDisplay.setAttribute('aria-label', 'Duration: ' + TimeUtils.formatDuration(duration));
1838
+ this.controls.durationDisplay.setAttribute('aria-label', i18n.t('time.durationPrefix') + TimeUtils.formatDuration(duration));
1839
1839
  }
1840
1840
  }
1841
1841