vidply 1.0.2 → 1.0.4

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.
@@ -3,1104 +3,1130 @@
3
3
  * Main Player Class
4
4
  */
5
5
 
6
- import { EventEmitter } from '../utils/EventEmitter.js';
7
- import { DOMUtils } from '../utils/DOMUtils.js';
8
- import { TimeUtils } from '../utils/TimeUtils.js';
9
- import { ControlBar } from '../controls/ControlBar.js';
10
- import { CaptionManager } from '../controls/CaptionManager.js';
11
- import { KeyboardManager } from '../controls/KeyboardManager.js';
12
- import { SettingsDialog } from '../controls/SettingsDialog.js';
13
- import { TranscriptManager } from '../controls/TranscriptManager.js';
14
- import { HTML5Renderer } from '../renderers/HTML5Renderer.js';
15
- import { YouTubeRenderer } from '../renderers/YouTubeRenderer.js';
16
- import { VimeoRenderer } from '../renderers/VimeoRenderer.js';
17
- import { HLSRenderer } from '../renderers/HLSRenderer.js';
18
- import { createPlayOverlay } from '../icons/Icons.js';
19
- import { i18n } from '../i18n/i18n.js';
6
+ import {EventEmitter} from '../utils/EventEmitter.js';
7
+ import {DOMUtils} from '../utils/DOMUtils.js';
8
+ import {ControlBar} from '../controls/ControlBar.js';
9
+ import {CaptionManager} from '../controls/CaptionManager.js';
10
+ import {KeyboardManager} from '../controls/KeyboardManager.js';
11
+ import {TranscriptManager} from '../controls/TranscriptManager.js';
12
+ import {HTML5Renderer} from '../renderers/HTML5Renderer.js';
13
+ import {YouTubeRenderer} from '../renderers/YouTubeRenderer.js';
14
+ import {VimeoRenderer} from '../renderers/VimeoRenderer.js';
15
+ import {HLSRenderer} from '../renderers/HLSRenderer.js';
16
+ import {createPlayOverlay} from '../icons/Icons.js';
17
+ import {i18n} from '../i18n/i18n.js';
20
18
 
21
19
  export class Player extends EventEmitter {
22
- constructor(element, options = {}) {
23
- super();
24
-
25
- this.element = typeof element === 'string' ? document.querySelector(element) : element;
26
- if (!this.element) {
27
- throw new Error('VidPly: Element not found');
28
- }
20
+ constructor(element, options = {}) {
21
+ super();
29
22
 
30
- // Auto-create media element if a non-media element is provided
31
- if (this.element.tagName !== 'VIDEO' && this.element.tagName !== 'AUDIO') {
32
- const mediaType = options.mediaType || 'video';
33
- const mediaElement = document.createElement(mediaType);
34
-
35
- // Copy attributes from the div to the media element
36
- Array.from(this.element.attributes).forEach(attr => {
37
- if (attr.name !== 'id' && attr.name !== 'class' && !attr.name.startsWith('data-')) {
38
- mediaElement.setAttribute(attr.name, attr.value);
23
+ this.element = typeof element === 'string' ? document.querySelector(element) : element;
24
+ if (!this.element) {
25
+ throw new Error('VidPly: Element not found');
39
26
  }
40
- });
41
-
42
- // Copy any track elements from the div
43
- const tracks = this.element.querySelectorAll('track');
44
- tracks.forEach(track => {
45
- mediaElement.appendChild(track.cloneNode(true));
46
- });
47
-
48
- // Clear the div and insert the media element
49
- this.element.innerHTML = '';
50
- this.element.appendChild(mediaElement);
51
-
52
- // Update element reference to the actual media element
53
- this.element = mediaElement;
54
- }
55
27
 
56
- // Default options
57
- this.options = {
58
- // Display
59
- width: null,
60
- height: null,
61
- poster: null,
62
- responsive: true,
63
- fillContainer: false,
64
-
65
- // Playback
66
- autoplay: false,
67
- loop: false,
68
- muted: false,
69
- volume: 0.8,
70
- playbackSpeed: 1.0,
71
- preload: 'metadata',
72
- startTime: 0,
73
-
74
- // Controls
75
- controls: true,
76
- hideControlsDelay: 3000,
77
- playPauseButton: true,
78
- progressBar: true,
79
- currentTime: true,
80
- duration: true,
81
- volumeControl: true,
82
- muteButton: true,
83
- chaptersButton: true,
84
- qualityButton: true,
85
- captionStyleButton: true,
86
- speedButton: true,
87
- captionsButton: true,
88
- transcriptButton: true,
89
- fullscreenButton: true,
90
- pipButton: true,
91
-
92
- // Seeking
93
- seekInterval: 10,
94
- seekIntervalLarge: 30,
95
-
96
- // Captions
97
- captions: true,
98
- captionsDefault: false,
99
- captionsFontSize: '100%',
100
- captionsFontFamily: 'sans-serif',
101
- captionsColor: '#FFFFFF',
102
- captionsBackgroundColor: '#000000',
103
- captionsOpacity: 0.8,
104
-
105
- // Audio Description
106
- audioDescription: true,
107
- audioDescriptionSrc: null, // URL to audio-described version
108
- audioDescriptionButton: true,
109
-
110
- // Sign Language
111
- signLanguage: true,
112
- signLanguageSrc: null, // URL to sign language video
113
- signLanguageButton: true,
114
- signLanguagePosition: 'bottom-right', // Position: 'bottom-right', 'bottom-left', 'top-right', 'top-left'
115
-
116
- // Transcripts
117
- transcript: false,
118
- transcriptPosition: 'external',
119
- transcriptContainer: null,
120
-
121
- // Keyboard
122
- keyboard: true,
123
- keyboardShortcuts: {
124
- 'play-pause': [' ', 'p', 'k'],
125
- 'volume-up': ['ArrowUp'],
126
- 'volume-down': ['ArrowDown'],
127
- 'seek-forward': ['ArrowRight', 'f'],
128
- 'seek-backward': ['ArrowLeft', 'r'],
129
- 'seek-forward-large': ['l'],
130
- 'seek-backward-large': ['j'],
131
- 'mute': ['m'],
132
- 'fullscreen': ['f'],
133
- 'captions': ['c'],
134
- 'speed-up': ['>'],
135
- 'speed-down': ['<'],
136
- 'settings': ['s']
137
- },
138
-
139
- // Accessibility
140
- ariaLabels: {},
141
- screenReaderAnnouncements: true,
142
- highContrast: false,
143
- focusHighlight: true,
144
-
145
- // Languages
146
- language: 'en',
147
- languages: ['en'],
148
-
149
- // Advanced
150
- debug: false,
151
- classPrefix: 'vidply',
152
- iconType: 'svg',
153
- pauseOthersOnPlay: true,
154
-
155
- // Callbacks
156
- onReady: null,
157
- onPlay: null,
158
- onPause: null,
159
- onEnded: null,
160
- onTimeUpdate: null,
161
- onVolumeChange: null,
162
- onError: null,
163
-
164
- ...options
165
- };
166
-
167
- // State
168
- this.state = {
169
- ready: false,
170
- playing: false,
171
- paused: true,
172
- ended: false,
173
- buffering: false,
174
- seeking: false,
175
- muted: this.options.muted,
176
- volume: this.options.volume,
177
- currentTime: 0,
178
- duration: 0,
179
- playbackSpeed: this.options.playbackSpeed,
180
- fullscreen: false,
181
- pip: false,
182
- captionsEnabled: this.options.captionsDefault,
183
- currentCaption: null,
184
- controlsVisible: true,
185
- audioDescriptionEnabled: false,
186
- signLanguageEnabled: false
187
- };
188
-
189
- // Store original source for toggling
190
- this.originalSrc = null;
191
- this.audioDescriptionSrc = this.options.audioDescriptionSrc;
192
- this.signLanguageSrc = this.options.signLanguageSrc;
193
- this.signLanguageVideo = null;
194
-
195
- // Components
196
- this.container = null;
197
- this.renderer = null;
198
- this.controlBar = null;
199
- this.captionManager = null;
200
- this.keyboardManager = null;
201
- this.settingsDialog = null;
202
-
203
- // Initialize
204
- this.init();
205
- }
206
-
207
- async init() {
208
- try {
209
- this.log('Initializing VidPly player');
210
-
211
- // Auto-detect language from HTML lang attribute if not explicitly set
212
- if (!this.options.language || this.options.language === 'en') {
213
- const htmlLang = this.detectHtmlLanguage();
214
- if (htmlLang) {
215
- this.options.language = htmlLang;
216
- this.log(`Auto-detected language from HTML: ${htmlLang}`);
28
+ // Auto-create media element if a non-media element is provided
29
+ if (this.element.tagName !== 'VIDEO' && this.element.tagName !== 'AUDIO') {
30
+ const mediaType = options.mediaType || 'video';
31
+ const mediaElement = document.createElement(mediaType);
32
+
33
+ // Copy attributes from the div to the media element
34
+ Array.from(this.element.attributes).forEach(attr => {
35
+ if (attr.name !== 'id' && attr.name !== 'class' && !attr.name.startsWith('data-')) {
36
+ mediaElement.setAttribute(attr.name, attr.value);
37
+ }
38
+ });
39
+
40
+ // Copy any track elements from the div
41
+ const tracks = this.element.querySelectorAll('track');
42
+ tracks.forEach(track => {
43
+ mediaElement.appendChild(track.cloneNode(true));
44
+ });
45
+
46
+ // Clear the div and insert the media element
47
+ this.element.innerHTML = '';
48
+ this.element.appendChild(mediaElement);
49
+
50
+ // Update element reference to the actual media element
51
+ this.element = mediaElement;
217
52
  }
218
- }
219
-
220
- // Set language
221
- i18n.setLanguage(this.options.language);
222
-
223
- // Create container
224
- this.createContainer();
225
-
226
- // Detect and initialize renderer (only if source exists)
227
- const src = this.element.src || this.element.querySelector('source')?.src;
228
- if (src) {
229
- await this.initializeRenderer();
230
- } else {
231
- this.log('No initial source - waiting for playlist or manual load');
232
- }
233
-
234
- // Create controls
235
- if (this.options.controls) {
236
- this.controlBar = new ControlBar(this);
237
- this.videoWrapper.appendChild(this.controlBar.element);
238
- }
239
-
240
- // Initialize captions
241
- if (this.options.captions) {
242
- this.captionManager = new CaptionManager(this);
243
- }
244
-
245
- // Initialize transcript
246
- if (this.options.transcript || this.options.transcriptButton) {
247
- this.transcriptManager = new TranscriptManager(this);
248
- }
249
-
250
- // Initialize keyboard controls
251
- if (this.options.keyboard) {
252
- this.keyboardManager = new KeyboardManager(this);
253
- }
254
-
255
- // Initialize settings dialog
256
- if (this.options.settingsButton) {
257
- this.settingsDialog = new SettingsDialog(this);
258
- }
259
-
260
- // Setup responsive handlers
261
- this.setupResponsiveHandlers();
262
-
263
- // Set initial state
264
- if (this.options.startTime > 0) {
265
- this.seek(this.options.startTime);
266
- }
267
-
268
- if (this.options.muted) {
269
- this.mute();
270
- }
271
-
272
- if (this.options.volume !== 0.8) {
273
- this.setVolume(this.options.volume);
274
- }
275
-
276
- // Mark as ready
277
- this.state.ready = true;
278
- this.emit('ready');
279
-
280
- if (this.options.onReady) {
281
- this.options.onReady.call(this);
282
- }
283
-
284
- // Autoplay if enabled
285
- if (this.options.autoplay) {
286
- this.play();
287
- }
288
-
289
- this.log('Player initialized successfully');
290
- } catch (error) {
291
- this.handleError(error);
53
+
54
+ // Default options
55
+ this.options = {
56
+ // Display
57
+ width: null,
58
+ height: null,
59
+ poster: null,
60
+ responsive: true,
61
+ fillContainer: false,
62
+
63
+ // Playback
64
+ autoplay: false,
65
+ loop: false,
66
+ muted: false,
67
+ volume: 0.8,
68
+ playbackSpeed: 1.0,
69
+ preload: 'metadata',
70
+ startTime: 0,
71
+
72
+ // Controls
73
+ controls: true,
74
+ hideControlsDelay: 3000,
75
+ playPauseButton: true,
76
+ progressBar: true,
77
+ currentTime: true,
78
+ duration: true,
79
+ volumeControl: true,
80
+ muteButton: true,
81
+ chaptersButton: true,
82
+ qualityButton: true,
83
+ captionStyleButton: true,
84
+ speedButton: true,
85
+ captionsButton: true,
86
+ transcriptButton: true,
87
+ fullscreenButton: true,
88
+ pipButton: true,
89
+
90
+ // Seeking
91
+ seekInterval: 10,
92
+ seekIntervalLarge: 30,
93
+
94
+ // Captions
95
+ captions: true,
96
+ captionsDefault: false,
97
+ captionsFontSize: '100%',
98
+ captionsFontFamily: 'sans-serif',
99
+ captionsColor: '#FFFFFF',
100
+ captionsBackgroundColor: '#000000',
101
+ captionsOpacity: 0.8,
102
+
103
+ // Audio Description
104
+ audioDescription: true,
105
+ audioDescriptionSrc: null, // URL to audio-described version
106
+ audioDescriptionButton: true,
107
+
108
+ // Sign Language
109
+ signLanguage: true,
110
+ signLanguageSrc: null, // URL to sign language video
111
+ signLanguageButton: true,
112
+ signLanguagePosition: 'bottom-right', // Position: 'bottom-right', 'bottom-left', 'top-right', 'top-left'
113
+
114
+ // Transcripts
115
+ transcript: false,
116
+ transcriptPosition: 'external',
117
+ transcriptContainer: null,
118
+
119
+ // Keyboard
120
+ keyboard: true,
121
+ keyboardShortcuts: {
122
+ 'play-pause': [' ', 'p', 'k'],
123
+ 'volume-up': ['ArrowUp'],
124
+ 'volume-down': ['ArrowDown'],
125
+ 'seek-forward': ['ArrowRight'],
126
+ 'seek-backward': ['ArrowLeft'],
127
+ 'mute': ['m'],
128
+ 'fullscreen': ['f'],
129
+ 'captions': ['c'],
130
+ 'caption-style-menu': ['a'],
131
+ 'speed-up': ['>'],
132
+ 'speed-down': ['<'],
133
+ 'speed-menu': ['s'],
134
+ 'quality-menu': ['q'],
135
+ 'chapters-menu': ['j'],
136
+ 'transcript-toggle': ['t']
137
+ },
138
+
139
+ // Accessibility
140
+ ariaLabels: {},
141
+ screenReaderAnnouncements: true,
142
+ highContrast: false,
143
+ focusHighlight: true,
144
+
145
+ // Languages
146
+ language: 'en',
147
+ languages: ['en'],
148
+
149
+ // Advanced
150
+ debug: false,
151
+ classPrefix: 'vidply',
152
+ iconType: 'svg',
153
+ pauseOthersOnPlay: true,
154
+
155
+ // Callbacks
156
+ onReady: null,
157
+ onPlay: null,
158
+ onPause: null,
159
+ onEnded: null,
160
+ onTimeUpdate: null,
161
+ onVolumeChange: null,
162
+ onError: null,
163
+
164
+ ...options
165
+ };
166
+
167
+ // State
168
+ this.state = {
169
+ ready: false,
170
+ playing: false,
171
+ paused: true,
172
+ ended: false,
173
+ buffering: false,
174
+ seeking: false,
175
+ muted: this.options.muted,
176
+ volume: this.options.volume,
177
+ currentTime: 0,
178
+ duration: 0,
179
+ playbackSpeed: this.options.playbackSpeed,
180
+ fullscreen: false,
181
+ pip: false,
182
+ captionsEnabled: this.options.captionsDefault,
183
+ currentCaption: null,
184
+ controlsVisible: true,
185
+ audioDescriptionEnabled: false,
186
+ signLanguageEnabled: false
187
+ };
188
+
189
+ // Store original source for toggling
190
+ this.originalSrc = null;
191
+ this.audioDescriptionSrc = this.options.audioDescriptionSrc;
192
+ this.signLanguageSrc = this.options.signLanguageSrc;
193
+ this.signLanguageVideo = null;
194
+
195
+ // Components
196
+ this.container = null;
197
+ this.renderer = null;
198
+ this.controlBar = null;
199
+ this.captionManager = null;
200
+ this.keyboardManager = null;
201
+ this.settingsDialog = null;
202
+
203
+ // Initialize
204
+ this.init();
292
205
  }
293
- }
294
-
295
- /**
296
- * Detect language from HTML lang attribute
297
- * @returns {string|null} Language code if available in translations, null otherwise
298
- */
299
- detectHtmlLanguage() {
300
- // Try to get lang from html element
301
- const htmlLang = document.documentElement.lang || document.documentElement.getAttribute('lang');
302
-
303
- if (!htmlLang) {
304
- return null;
206
+
207
+ async init() {
208
+ try {
209
+ this.log('Initializing VidPly player');
210
+
211
+ // Auto-detect language from HTML lang attribute if not explicitly set
212
+ if (!this.options.language || this.options.language === 'en') {
213
+ const htmlLang = this.detectHtmlLanguage();
214
+ if (htmlLang) {
215
+ this.options.language = htmlLang;
216
+ this.log(`Auto-detected language from HTML: ${htmlLang}`);
217
+ }
218
+ }
219
+
220
+ // Set language
221
+ i18n.setLanguage(this.options.language);
222
+
223
+ // Create container
224
+ this.createContainer();
225
+
226
+ // Detect and initialize renderer (only if source exists)
227
+ const src = this.element.src || this.element.querySelector('source')?.src;
228
+ if (src) {
229
+ await this.initializeRenderer();
230
+ } else {
231
+ this.log('No initial source - waiting for playlist or manual load');
232
+ }
233
+
234
+ // Create controls
235
+ if (this.options.controls) {
236
+ this.controlBar = new ControlBar(this);
237
+ this.videoWrapper.appendChild(this.controlBar.element);
238
+ }
239
+
240
+ // Initialize captions
241
+ if (this.options.captions) {
242
+ this.captionManager = new CaptionManager(this);
243
+ }
244
+
245
+ // Initialize transcript
246
+ if (this.options.transcript || this.options.transcriptButton) {
247
+ this.transcriptManager = new TranscriptManager(this);
248
+ }
249
+
250
+ // Initialize keyboard controls
251
+ if (this.options.keyboard) {
252
+ this.keyboardManager = new KeyboardManager(this);
253
+ }
254
+
255
+ // Setup responsive handlers
256
+ this.setupResponsiveHandlers();
257
+
258
+ // Set initial state
259
+ if (this.options.startTime > 0) {
260
+ this.seek(this.options.startTime);
261
+ }
262
+
263
+ if (this.options.muted) {
264
+ this.mute();
265
+ }
266
+
267
+ if (this.options.volume !== 0.8) {
268
+ this.setVolume(this.options.volume);
269
+ }
270
+
271
+ // Mark as ready
272
+ this.state.ready = true;
273
+ this.emit('ready');
274
+
275
+ if (this.options.onReady) {
276
+ this.options.onReady.call(this);
277
+ }
278
+
279
+ // Autoplay if enabled
280
+ if (this.options.autoplay) {
281
+ this.play();
282
+ }
283
+
284
+ this.log('Player initialized successfully');
285
+ } catch (error) {
286
+ this.handleError(error);
287
+ }
305
288
  }
306
-
307
- // Normalize the language code (e.g., "en-US" -> "en", "de-DE" -> "de")
308
- const normalizedLang = htmlLang.toLowerCase().split('-')[0];
309
-
310
- // Check if this language is available in our translations
311
- const availableLanguages = ['en', 'de', 'es', 'fr', 'ja'];
312
-
313
- if (availableLanguages.includes(normalizedLang)) {
314
- return normalizedLang;
289
+
290
+ /**
291
+ * Detect language from HTML lang attribute
292
+ * @returns {string|null} Language code if available in translations, null otherwise
293
+ */
294
+ detectHtmlLanguage() {
295
+ // Try to get lang from html element
296
+ const htmlLang = document.documentElement.lang || document.documentElement.getAttribute('lang');
297
+
298
+ if (!htmlLang) {
299
+ return null;
300
+ }
301
+
302
+ // Normalize the language code (e.g., "en-US" -> "en", "de-DE" -> "de")
303
+ const normalizedLang = htmlLang.toLowerCase().split('-')[0];
304
+
305
+ // Check if this language is available in our translations
306
+ const availableLanguages = ['en', 'de', 'es', 'fr', 'ja'];
307
+
308
+ if (availableLanguages.includes(normalizedLang)) {
309
+ return normalizedLang;
310
+ }
311
+
312
+ // Language not available, will fallback to English
313
+ this.log(`Language "${htmlLang}" not available, using English as fallback`);
314
+ return 'en';
315
315
  }
316
-
317
- // Language not available, will fallback to English
318
- this.log(`Language "${htmlLang}" not available, using English as fallback`);
319
- return 'en';
320
- }
321
-
322
- createContainer() {
323
- // Create main container
324
- this.container = DOMUtils.createElement('div', {
325
- className: `${this.options.classPrefix}-player`,
326
- attributes: {
327
- 'role': 'region',
328
- 'aria-label': i18n.t('player.label'),
329
- 'tabindex': '0'
330
- }
331
- });
332
-
333
- // Add media type class
334
- const mediaType = this.element.tagName.toLowerCase();
335
- this.container.classList.add(`${this.options.classPrefix}-${mediaType}`);
336
-
337
- // Add responsive class
338
- if (this.options.responsive) {
339
- this.container.classList.add(`${this.options.classPrefix}-responsive`);
316
+
317
+ createContainer() {
318
+ // Create main container
319
+ this.container = DOMUtils.createElement('div', {
320
+ className: `${this.options.classPrefix}-player`,
321
+ attributes: {
322
+ 'role': 'region',
323
+ 'aria-label': i18n.t('player.label'),
324
+ 'tabindex': '0'
325
+ }
326
+ });
327
+
328
+ // Add media type class
329
+ const mediaType = this.element.tagName.toLowerCase();
330
+ this.container.classList.add(`${this.options.classPrefix}-${mediaType}`);
331
+
332
+ // Add responsive class
333
+ if (this.options.responsive) {
334
+ this.container.classList.add(`${this.options.classPrefix}-responsive`);
335
+ }
336
+
337
+ // Create video wrapper (for proper positioning of controls)
338
+ this.videoWrapper = DOMUtils.createElement('div', {
339
+ className: `${this.options.classPrefix}-video-wrapper`
340
+ });
341
+
342
+ // Wrap original element
343
+ this.element.parentNode.insertBefore(this.container, this.element);
344
+ this.container.appendChild(this.videoWrapper);
345
+ this.videoWrapper.appendChild(this.element);
346
+
347
+ // Hide native controls and set dimensions
348
+ this.element.controls = false;
349
+ this.element.removeAttribute('controls');
350
+ this.element.setAttribute('tabindex', '-1'); // Remove from tab order
351
+ this.element.style.width = '100%';
352
+ this.element.style.height = '100%';
353
+
354
+ // Set dimensions
355
+ if (this.options.width) {
356
+ this.container.style.width = typeof this.options.width === 'number'
357
+ ? `${this.options.width}px`
358
+ : this.options.width;
359
+ }
360
+
361
+ if (this.options.height) {
362
+ this.container.style.height = typeof this.options.height === 'number'
363
+ ? `${this.options.height}px`
364
+ : this.options.height;
365
+ }
366
+
367
+ // Set poster
368
+ if (this.options.poster && this.element.tagName === 'VIDEO') {
369
+ this.element.poster = this.options.poster;
370
+ }
371
+
372
+ // Create centered play button overlay (only for video)
373
+ if (this.element.tagName === 'VIDEO') {
374
+ this.createPlayButtonOverlay();
375
+ }
376
+
377
+ // Make video/audio element clickable to toggle play/pause
378
+ this.element.style.cursor = 'pointer';
379
+ this.element.addEventListener('click', (e) => {
380
+ // Prevent if clicking on native controls (shouldn't happen but just in case)
381
+ if (e.target === this.element) {
382
+ this.toggle();
383
+ }
384
+ });
340
385
  }
341
386
 
342
- // Create video wrapper (for proper positioning of controls)
343
- this.videoWrapper = DOMUtils.createElement('div', {
344
- className: `${this.options.classPrefix}-video-wrapper`
345
- });
387
+ createPlayButtonOverlay() {
388
+ // Create complete SVG play button from Icons.js
389
+ this.playButtonOverlay = createPlayOverlay();
346
390
 
347
- // Wrap original element
348
- this.element.parentNode.insertBefore(this.container, this.element);
349
- this.container.appendChild(this.videoWrapper);
350
- this.videoWrapper.appendChild(this.element);
351
-
352
- // Hide native controls and set dimensions
353
- this.element.controls = false;
354
- this.element.removeAttribute('controls');
355
- this.element.setAttribute('tabindex', '-1'); // Remove from tab order
356
- this.element.style.width = '100%';
357
- this.element.style.height = '100%';
358
-
359
- // Set dimensions
360
- if (this.options.width) {
361
- this.container.style.width = typeof this.options.width === 'number'
362
- ? `${this.options.width}px`
363
- : this.options.width;
391
+ // Add click handler
392
+ this.playButtonOverlay.addEventListener('click', () => {
393
+ this.toggle();
394
+ });
395
+
396
+ // Add to video wrapper
397
+ this.videoWrapper.appendChild(this.playButtonOverlay);
398
+
399
+ // Show/hide based on play state
400
+ this.on('play', () => {
401
+ this.playButtonOverlay.style.opacity = '0';
402
+ this.playButtonOverlay.style.pointerEvents = 'none';
403
+ });
404
+
405
+ this.on('pause', () => {
406
+ this.playButtonOverlay.style.opacity = '1';
407
+ this.playButtonOverlay.style.pointerEvents = 'auto';
408
+ });
409
+
410
+ this.on('ended', () => {
411
+ this.playButtonOverlay.style.opacity = '1';
412
+ this.playButtonOverlay.style.pointerEvents = 'auto';
413
+ });
364
414
  }
365
-
366
- if (this.options.height) {
367
- this.container.style.height = typeof this.options.height === 'number'
368
- ? `${this.options.height}px`
369
- : this.options.height;
415
+
416
+ async initializeRenderer() {
417
+ const src = this.element.src || this.element.querySelector('source')?.src;
418
+
419
+ if (!src) {
420
+ throw new Error('No media source found');
421
+ }
422
+
423
+ // Store original source for audio description toggling
424
+ if (!this.originalSrc) {
425
+ this.originalSrc = src;
426
+ }
427
+
428
+ // Detect media type
429
+ let renderer;
430
+
431
+ if (src.includes('youtube.com') || src.includes('youtu.be')) {
432
+ renderer = YouTubeRenderer;
433
+ } else if (src.includes('vimeo.com')) {
434
+ renderer = VimeoRenderer;
435
+ } else if (src.includes('.m3u8')) {
436
+ renderer = HLSRenderer;
437
+ } else {
438
+ renderer = HTML5Renderer;
439
+ }
440
+
441
+ this.log(`Using ${renderer.name} renderer`);
442
+ this.renderer = new renderer(this);
443
+ await this.renderer.init();
370
444
  }
371
-
372
- // Set poster
373
- if (this.options.poster && this.element.tagName === 'VIDEO') {
374
- this.element.poster = this.options.poster;
445
+
446
+ /**
447
+ * Load new media source (for playlists)
448
+ * @param {Object} config - Media configuration
449
+ * @param {string} config.src - Media source URL
450
+ * @param {string} config.type - Media MIME type
451
+ * @param {string} [config.poster] - Poster image URL
452
+ * @param {Array} [config.tracks] - Text tracks (captions, chapters, etc.)
453
+ */
454
+ async load(config) {
455
+ try {
456
+ this.log('Loading new media:', config.src);
457
+
458
+ // Pause current playback
459
+ if (this.renderer) {
460
+ this.pause();
461
+ }
462
+
463
+ // Clear existing text tracks
464
+ const existingTracks = this.element.querySelectorAll('track');
465
+ existingTracks.forEach(track => track.remove());
466
+
467
+ // Update media element
468
+ this.element.src = config.src;
469
+
470
+ if (config.type) {
471
+ this.element.type = config.type;
472
+ }
473
+
474
+ if (config.poster && this.element.tagName === 'VIDEO') {
475
+ this.element.poster = config.poster;
476
+ }
477
+
478
+ // Add new text tracks
479
+ if (config.tracks && config.tracks.length > 0) {
480
+ config.tracks.forEach(trackConfig => {
481
+ const track = document.createElement('track');
482
+ track.src = trackConfig.src;
483
+ track.kind = trackConfig.kind || 'captions';
484
+ track.srclang = trackConfig.srclang || 'en';
485
+ track.label = trackConfig.label || trackConfig.srclang;
486
+
487
+ if (trackConfig.default) {
488
+ track.default = true;
489
+ }
490
+
491
+ this.element.appendChild(track);
492
+ });
493
+ }
494
+
495
+ // Check if we need to change renderer type
496
+ const shouldChangeRenderer = this.shouldChangeRenderer(config.src);
497
+
498
+ // Destroy old renderer if changing types
499
+ if (shouldChangeRenderer && this.renderer) {
500
+ this.renderer.destroy();
501
+ this.renderer = null;
502
+ }
503
+
504
+ // Initialize or reinitialize renderer
505
+ if (!this.renderer || shouldChangeRenderer) {
506
+ await this.initializeRenderer();
507
+ } else {
508
+ // Just reload the current renderer with the updated element
509
+ this.renderer.media = this.element; // Update media reference
510
+ this.element.load();
511
+ }
512
+
513
+ // Reinitialize caption manager to pick up new tracks
514
+ if (this.captionManager) {
515
+ this.captionManager.destroy();
516
+ this.captionManager = new CaptionManager(this);
517
+ }
518
+
519
+ // Reinitialize transcript manager to pick up new tracks
520
+ if (this.transcriptManager) {
521
+ const wasVisible = this.transcriptManager.isVisible;
522
+ this.transcriptManager.destroy();
523
+ this.transcriptManager = new TranscriptManager(this);
524
+
525
+ // Restore visibility state if transcript was open
526
+ if (wasVisible) {
527
+ this.transcriptManager.showTranscript();
528
+ }
529
+ }
530
+
531
+ // Update control bar to show/hide feature buttons based on new tracks
532
+ if (this.controlBar) {
533
+ this.updateControlBar();
534
+ }
535
+
536
+ this.emit('sourcechange', config);
537
+ this.log('Media loaded successfully');
538
+
539
+ } catch (error) {
540
+ this.handleError(error);
541
+ }
375
542
  }
376
-
377
- // Create centered play button overlay (only for video)
378
- if (this.element.tagName === 'VIDEO') {
379
- this.createPlayButtonOverlay();
543
+
544
+ /**
545
+ * Check if we need to change renderer type
546
+ * @param {string} src - New source URL
547
+ * @returns {boolean}
548
+ */
549
+ /**
550
+ * Update control bar to refresh button visibility based on available features
551
+ */
552
+ updateControlBar() {
553
+ if (!this.controlBar) return;
554
+
555
+ const controlBar = this.controlBar;
556
+
557
+ // Clear existing controls content
558
+ controlBar.element.innerHTML = '';
559
+
560
+ // Recreate controls with updated feature detection
561
+ controlBar.createControls();
562
+
563
+ // Reattach events for the new controls
564
+ controlBar.attachEvents();
565
+ controlBar.setupAutoHide();
380
566
  }
381
567
 
382
- // Make video/audio element clickable to toggle play/pause
383
- this.element.style.cursor = 'pointer';
384
- this.element.addEventListener('click', (e) => {
385
- // Prevent if clicking on native controls (shouldn't happen but just in case)
386
- if (e.target === this.element) {
387
- this.toggle();
388
- }
389
- });
390
- }
391
-
392
- createPlayButtonOverlay() {
393
- // Create complete SVG play button from Icons.js
394
- this.playButtonOverlay = createPlayOverlay();
395
-
396
- // Add click handler
397
- this.playButtonOverlay.addEventListener('click', () => {
398
- this.toggle();
399
- });
400
-
401
- // Add to video wrapper
402
- this.videoWrapper.appendChild(this.playButtonOverlay);
403
-
404
- // Show/hide based on play state
405
- this.on('play', () => {
406
- this.playButtonOverlay.style.opacity = '0';
407
- this.playButtonOverlay.style.pointerEvents = 'none';
408
- });
409
-
410
- this.on('pause', () => {
411
- this.playButtonOverlay.style.opacity = '1';
412
- this.playButtonOverlay.style.pointerEvents = 'auto';
413
- });
414
-
415
- this.on('ended', () => {
416
- this.playButtonOverlay.style.opacity = '1';
417
- this.playButtonOverlay.style.pointerEvents = 'auto';
418
- });
419
- }
420
-
421
- async initializeRenderer() {
422
- const src = this.element.src || this.element.querySelector('source')?.src;
423
-
424
- if (!src) {
425
- throw new Error('No media source found');
568
+ shouldChangeRenderer(src) {
569
+ if (!this.renderer) return true;
570
+
571
+ const isYouTube = src.includes('youtube.com') || src.includes('youtu.be');
572
+ const isVimeo = src.includes('vimeo.com');
573
+ const isHLS = src.includes('.m3u8');
574
+
575
+ const currentRendererName = this.renderer.constructor.name;
576
+
577
+ if (isYouTube && currentRendererName !== 'YouTubeRenderer') return true;
578
+ if (isVimeo && currentRendererName !== 'VimeoRenderer') return true;
579
+ if (isHLS && currentRendererName !== 'HLSRenderer') return true;
580
+ if (!isYouTube && !isVimeo && !isHLS && currentRendererName !== 'HTML5Renderer') return true;
581
+
582
+ return false;
426
583
  }
427
584
 
428
- // Store original source for audio description toggling
429
- if (!this.originalSrc) {
430
- this.originalSrc = src;
585
+ // Playback controls
586
+ play() {
587
+ if (this.renderer) {
588
+ this.renderer.play();
589
+ }
431
590
  }
432
591
 
433
- // Detect media type
434
- let renderer;
435
-
436
- if (src.includes('youtube.com') || src.includes('youtu.be')) {
437
- renderer = YouTubeRenderer;
438
- } else if (src.includes('vimeo.com')) {
439
- renderer = VimeoRenderer;
440
- } else if (src.includes('.m3u8')) {
441
- renderer = HLSRenderer;
442
- } else {
443
- renderer = HTML5Renderer;
592
+ pause() {
593
+ if (this.renderer) {
594
+ this.renderer.pause();
595
+ }
444
596
  }
445
597
 
446
- this.log(`Using ${renderer.name} renderer`);
447
- this.renderer = new renderer(this);
448
- await this.renderer.init();
449
- }
450
-
451
- /**
452
- * Load new media source (for playlists)
453
- * @param {Object} config - Media configuration
454
- * @param {string} config.src - Media source URL
455
- * @param {string} config.type - Media MIME type
456
- * @param {string} [config.poster] - Poster image URL
457
- * @param {Array} [config.tracks] - Text tracks (captions, chapters, etc.)
458
- */
459
- async load(config) {
460
- try {
461
- this.log('Loading new media:', config.src);
462
-
463
- // Pause current playback
464
- if (this.renderer) {
598
+ stop() {
465
599
  this.pause();
466
- }
467
-
468
- // Clear existing text tracks
469
- const existingTracks = this.element.querySelectorAll('track');
470
- existingTracks.forEach(track => track.remove());
471
-
472
- // Update media element
473
- this.element.src = config.src;
474
-
475
- if (config.type) {
476
- this.element.type = config.type;
477
- }
478
-
479
- if (config.poster && this.element.tagName === 'VIDEO') {
480
- this.element.poster = config.poster;
481
- }
482
-
483
- // Add new text tracks
484
- if (config.tracks && config.tracks.length > 0) {
485
- config.tracks.forEach(trackConfig => {
486
- const track = document.createElement('track');
487
- track.src = trackConfig.src;
488
- track.kind = trackConfig.kind || 'captions';
489
- track.srclang = trackConfig.srclang || 'en';
490
- track.label = trackConfig.label || trackConfig.srclang;
491
-
492
- if (trackConfig.default) {
493
- track.default = true;
494
- }
495
-
496
- this.element.appendChild(track);
497
- });
498
- }
499
-
500
- // Check if we need to change renderer type
501
- const shouldChangeRenderer = this.shouldChangeRenderer(config.src);
502
-
503
- // Destroy old renderer if changing types
504
- if (shouldChangeRenderer && this.renderer) {
505
- this.renderer.destroy();
506
- this.renderer = null;
507
- }
508
-
509
- // Initialize or reinitialize renderer
510
- if (!this.renderer || shouldChangeRenderer) {
511
- await this.initializeRenderer();
512
- } else {
513
- // Just reload the current renderer with the updated element
514
- this.renderer.media = this.element; // Update media reference
515
- this.element.load();
516
- }
517
-
518
- // Reinitialize caption manager to pick up new tracks
519
- if (this.captionManager) {
520
- this.captionManager.destroy();
521
- this.captionManager = new CaptionManager(this);
522
- }
523
-
524
- this.emit('sourcechange', config);
525
- this.log('Media loaded successfully');
526
-
527
- } catch (error) {
528
- this.handleError(error);
600
+ this.seek(0);
529
601
  }
530
- }
531
-
532
- /**
533
- * Check if we need to change renderer type
534
- * @param {string} src - New source URL
535
- * @returns {boolean}
536
- */
537
- shouldChangeRenderer(src) {
538
- if (!this.renderer) return true;
539
-
540
- const isYouTube = src.includes('youtube.com') || src.includes('youtu.be');
541
- const isVimeo = src.includes('vimeo.com');
542
- const isHLS = src.includes('.m3u8');
543
-
544
- const currentRendererName = this.renderer.constructor.name;
545
-
546
- if (isYouTube && currentRendererName !== 'YouTubeRenderer') return true;
547
- if (isVimeo && currentRendererName !== 'VimeoRenderer') return true;
548
- if (isHLS && currentRendererName !== 'HLSRenderer') return true;
549
- if (!isYouTube && !isVimeo && !isHLS && currentRendererName !== 'HTML5Renderer') return true;
550
-
551
- return false;
552
- }
553
602
 
554
- // Playback controls
555
- play() {
556
- if (this.renderer) {
557
- this.renderer.play();
603
+ toggle() {
604
+ if (this.state.playing) {
605
+ this.pause();
606
+ } else {
607
+ this.play();
608
+ }
558
609
  }
559
- }
560
610
 
561
- pause() {
562
- if (this.renderer) {
563
- this.renderer.pause();
564
- }
565
- }
566
-
567
- stop() {
568
- this.pause();
569
- this.seek(0);
570
- }
571
-
572
- toggle() {
573
- if (this.state.playing) {
574
- this.pause();
575
- } else {
576
- this.play();
611
+ seek(time) {
612
+ if (this.renderer) {
613
+ this.renderer.seek(time);
614
+ }
577
615
  }
578
- }
579
616
 
580
- seek(time) {
581
- if (this.renderer) {
582
- this.renderer.seek(time);
617
+ seekForward(interval = this.options.seekInterval) {
618
+ this.seek(Math.min(this.state.currentTime + interval, this.state.duration));
583
619
  }
584
- }
585
620
 
586
- seekForward(interval = this.options.seekInterval) {
587
- this.seek(Math.min(this.state.currentTime + interval, this.state.duration));
588
- }
621
+ seekBackward(interval = this.options.seekInterval) {
622
+ this.seek(Math.max(this.state.currentTime - interval, 0));
623
+ }
589
624
 
590
- seekBackward(interval = this.options.seekInterval) {
591
- this.seek(Math.max(this.state.currentTime - interval, 0));
592
- }
625
+ // Volume controls
626
+ setVolume(volume) {
627
+ const newVolume = Math.max(0, Math.min(1, volume));
628
+ if (this.renderer) {
629
+ this.renderer.setVolume(newVolume);
630
+ }
631
+ this.state.volume = newVolume;
593
632
 
594
- // Volume controls
595
- setVolume(volume) {
596
- const newVolume = Math.max(0, Math.min(1, volume));
597
- if (this.renderer) {
598
- this.renderer.setVolume(newVolume);
633
+ if (newVolume > 0 && this.state.muted) {
634
+ this.state.muted = false;
635
+ }
599
636
  }
600
- this.state.volume = newVolume;
601
-
602
- if (newVolume > 0 && this.state.muted) {
603
- this.state.muted = false;
637
+
638
+ getVolume() {
639
+ return this.state.volume;
604
640
  }
605
- }
606
641
 
607
- getVolume() {
608
- return this.state.volume;
609
- }
642
+ mute() {
643
+ if (this.renderer) {
644
+ this.renderer.setMuted(true);
645
+ }
646
+ this.state.muted = true;
647
+ this.emit('volumechange');
648
+ }
610
649
 
611
- mute() {
612
- if (this.renderer) {
613
- this.renderer.setMuted(true);
650
+ unmute() {
651
+ if (this.renderer) {
652
+ this.renderer.setMuted(false);
653
+ }
654
+ this.state.muted = false;
655
+ this.emit('volumechange');
614
656
  }
615
- this.state.muted = true;
616
- }
617
657
 
618
- unmute() {
619
- if (this.renderer) {
620
- this.renderer.setMuted(false);
658
+ toggleMute() {
659
+ if (this.state.muted) {
660
+ this.unmute();
661
+ } else {
662
+ this.mute();
663
+ }
621
664
  }
622
- this.state.muted = false;
623
- }
624
-
625
- toggleMute() {
626
- if (this.state.muted) {
627
- this.unmute();
628
- } else {
629
- this.mute();
665
+
666
+ // Playback speed
667
+ setPlaybackSpeed(speed) {
668
+ const newSpeed = Math.max(0.25, Math.min(2, speed));
669
+ if (this.renderer) {
670
+ this.renderer.setPlaybackSpeed(newSpeed);
671
+ }
672
+ this.state.playbackSpeed = newSpeed;
673
+ this.emit('playbackspeedchange', newSpeed);
630
674
  }
631
- }
632
675
 
633
- // Playback speed
634
- setPlaybackSpeed(speed) {
635
- const newSpeed = Math.max(0.25, Math.min(2, speed));
636
- if (this.renderer) {
637
- this.renderer.setPlaybackSpeed(newSpeed);
676
+ getPlaybackSpeed() {
677
+ return this.state.playbackSpeed;
638
678
  }
639
- this.state.playbackSpeed = newSpeed;
640
- this.emit('playbackspeedchange', newSpeed);
641
- }
642
679
 
643
- getPlaybackSpeed() {
644
- return this.state.playbackSpeed;
645
- }
680
+ // Fullscreen
681
+ enterFullscreen() {
682
+ const elem = this.container;
683
+
684
+ if (elem.requestFullscreen) {
685
+ elem.requestFullscreen();
686
+ } else if (elem.webkitRequestFullscreen) {
687
+ elem.webkitRequestFullscreen();
688
+ } else if (elem.mozRequestFullScreen) {
689
+ elem.mozRequestFullScreen();
690
+ } else if (elem.msRequestFullscreen) {
691
+ elem.msRequestFullscreen();
692
+ }
646
693
 
647
- // Fullscreen
648
- enterFullscreen() {
649
- const elem = this.container;
650
-
651
- if (elem.requestFullscreen) {
652
- elem.requestFullscreen();
653
- } else if (elem.webkitRequestFullscreen) {
654
- elem.webkitRequestFullscreen();
655
- } else if (elem.mozRequestFullScreen) {
656
- elem.mozRequestFullScreen();
657
- } else if (elem.msRequestFullscreen) {
658
- elem.msRequestFullscreen();
659
- }
660
-
661
- this.state.fullscreen = true;
662
- this.container.classList.add(`${this.options.classPrefix}-fullscreen`);
663
- this.emit('fullscreenchange', true);
664
- }
665
-
666
- exitFullscreen() {
667
- if (document.exitFullscreen) {
668
- document.exitFullscreen();
669
- } else if (document.webkitExitFullscreen) {
670
- document.webkitExitFullscreen();
671
- } else if (document.mozCancelFullScreen) {
672
- document.mozCancelFullScreen();
673
- } else if (document.msExitFullscreen) {
674
- document.msExitFullscreen();
694
+ this.state.fullscreen = true;
695
+ this.container.classList.add(`${this.options.classPrefix}-fullscreen`);
696
+ this.emit('fullscreenchange', true);
675
697
  }
676
-
677
- this.state.fullscreen = false;
678
- this.container.classList.remove(`${this.options.classPrefix}-fullscreen`);
679
- this.emit('fullscreenchange', false);
680
- }
681
-
682
- toggleFullscreen() {
683
- if (this.state.fullscreen) {
684
- this.exitFullscreen();
685
- } else {
686
- this.enterFullscreen();
698
+
699
+ exitFullscreen() {
700
+ if (document.exitFullscreen) {
701
+ document.exitFullscreen();
702
+ } else if (document.webkitExitFullscreen) {
703
+ document.webkitExitFullscreen();
704
+ } else if (document.mozCancelFullScreen) {
705
+ document.mozCancelFullScreen();
706
+ } else if (document.msExitFullscreen) {
707
+ document.msExitFullscreen();
708
+ }
709
+
710
+ this.state.fullscreen = false;
711
+ this.container.classList.remove(`${this.options.classPrefix}-fullscreen`);
712
+ this.emit('fullscreenchange', false);
687
713
  }
688
- }
689
-
690
- // Picture-in-Picture
691
- enterPiP() {
692
- if (this.element.requestPictureInPicture) {
693
- this.element.requestPictureInPicture();
694
- this.state.pip = true;
695
- this.emit('pipchange', true);
714
+
715
+ toggleFullscreen() {
716
+ if (this.state.fullscreen) {
717
+ this.exitFullscreen();
718
+ } else {
719
+ this.enterFullscreen();
720
+ }
696
721
  }
697
- }
698
722
 
699
- exitPiP() {
700
- if (document.pictureInPictureElement) {
701
- document.exitPictureInPicture();
702
- this.state.pip = false;
703
- this.emit('pipchange', false);
723
+ // Picture-in-Picture
724
+ enterPiP() {
725
+ if (this.element.requestPictureInPicture) {
726
+ this.element.requestPictureInPicture();
727
+ this.state.pip = true;
728
+ this.emit('pipchange', true);
729
+ }
704
730
  }
705
- }
706
731
 
707
- togglePiP() {
708
- if (this.state.pip) {
709
- this.exitPiP();
710
- } else {
711
- this.enterPiP();
732
+ exitPiP() {
733
+ if (document.pictureInPictureElement) {
734
+ document.exitPictureInPicture();
735
+ this.state.pip = false;
736
+ this.emit('pipchange', false);
737
+ }
712
738
  }
713
- }
714
739
 
715
- // Captions
716
- enableCaptions() {
717
- if (this.captionManager) {
718
- this.captionManager.enable();
719
- this.state.captionsEnabled = true;
740
+ togglePiP() {
741
+ if (this.state.pip) {
742
+ this.exitPiP();
743
+ } else {
744
+ this.enterPiP();
745
+ }
720
746
  }
721
- }
722
747
 
723
- disableCaptions() {
724
- if (this.captionManager) {
725
- this.captionManager.disable();
726
- this.state.captionsEnabled = false;
748
+ // Captions
749
+ enableCaptions() {
750
+ if (this.captionManager) {
751
+ this.captionManager.enable();
752
+ this.state.captionsEnabled = true;
753
+ }
727
754
  }
728
- }
729
755
 
730
- toggleCaptions() {
731
- if (this.state.captionsEnabled) {
732
- this.disableCaptions();
733
- } else {
734
- this.enableCaptions();
756
+ disableCaptions() {
757
+ if (this.captionManager) {
758
+ this.captionManager.disable();
759
+ this.state.captionsEnabled = false;
760
+ }
735
761
  }
736
- }
737
762
 
738
- // Audio Description
739
- async enableAudioDescription() {
740
- if (!this.audioDescriptionSrc) {
741
- console.warn('VidPly: No audio description source provided');
742
- return;
763
+ toggleCaptions() {
764
+ if (this.state.captionsEnabled) {
765
+ this.disableCaptions();
766
+ } else {
767
+ this.enableCaptions();
768
+ }
743
769
  }
744
770
 
745
- // Store current playback state
746
- const currentTime = this.state.currentTime;
747
- const wasPlaying = this.state.playing;
771
+ // Audio Description
772
+ async enableAudioDescription() {
773
+ if (!this.audioDescriptionSrc) {
774
+ console.warn('VidPly: No audio description source provided');
775
+ return;
776
+ }
748
777
 
749
- // Switch to audio-described version
750
- this.element.src = this.audioDescriptionSrc;
751
-
752
- // Wait for new source to load
753
- await new Promise((resolve) => {
754
- const onLoadedMetadata = () => {
755
- this.element.removeEventListener('loadedmetadata', onLoadedMetadata);
756
- resolve();
757
- };
758
- this.element.addEventListener('loadedmetadata', onLoadedMetadata);
759
- });
760
-
761
- // Restore playback position
762
- this.seek(currentTime);
763
-
764
- if (wasPlaying) {
765
- this.play();
766
- }
778
+ // Store current playback state
779
+ const currentTime = this.state.currentTime;
780
+ const wasPlaying = this.state.playing;
767
781
 
768
- this.state.audioDescriptionEnabled = true;
769
- this.emit('audiodescriptionenabled');
770
- }
782
+ // Switch to audio-described version
783
+ this.element.src = this.audioDescriptionSrc;
771
784
 
772
- async disableAudioDescription() {
773
- if (!this.originalSrc) {
774
- return;
775
- }
785
+ // Wait for new source to load
786
+ await new Promise((resolve) => {
787
+ const onLoadedMetadata = () => {
788
+ this.element.removeEventListener('loadedmetadata', onLoadedMetadata);
789
+ resolve();
790
+ };
791
+ this.element.addEventListener('loadedmetadata', onLoadedMetadata);
792
+ });
776
793
 
777
- // Store current playback state
778
- const currentTime = this.state.currentTime;
779
- const wasPlaying = this.state.playing;
794
+ // Restore playback position
795
+ this.seek(currentTime);
780
796
 
781
- // Switch back to original version
782
- this.element.src = this.originalSrc;
783
-
784
- // Wait for new source to load
785
- await new Promise((resolve) => {
786
- const onLoadedMetadata = () => {
787
- this.element.removeEventListener('loadedmetadata', onLoadedMetadata);
788
- resolve();
789
- };
790
- this.element.addEventListener('loadedmetadata', onLoadedMetadata);
791
- });
792
-
793
- // Restore playback position
794
- this.seek(currentTime);
795
-
796
- if (wasPlaying) {
797
- this.play();
797
+ if (wasPlaying) {
798
+ this.play();
799
+ }
800
+
801
+ this.state.audioDescriptionEnabled = true;
802
+ this.emit('audiodescriptionenabled');
798
803
  }
799
804
 
800
- this.state.audioDescriptionEnabled = false;
801
- this.emit('audiodescriptiondisabled');
802
- }
805
+ async disableAudioDescription() {
806
+ if (!this.originalSrc) {
807
+ return;
808
+ }
803
809
 
804
- async toggleAudioDescription() {
805
- if (this.state.audioDescriptionEnabled) {
806
- await this.disableAudioDescription();
807
- } else {
808
- await this.enableAudioDescription();
809
- }
810
- }
810
+ // Store current playback state
811
+ const currentTime = this.state.currentTime;
812
+ const wasPlaying = this.state.playing;
811
813
 
812
- // Sign Language
813
- enableSignLanguage() {
814
- if (!this.signLanguageSrc) {
815
- console.warn('No sign language video source provided');
816
- return;
817
- }
814
+ // Switch back to original version
815
+ this.element.src = this.originalSrc;
818
816
 
819
- if (this.signLanguageVideo) {
820
- // Already exists, just show it
821
- this.signLanguageVideo.style.display = 'block';
822
- this.state.signLanguageEnabled = true;
823
- this.emit('signlanguageenabled');
824
- return;
825
- }
817
+ // Wait for new source to load
818
+ await new Promise((resolve) => {
819
+ const onLoadedMetadata = () => {
820
+ this.element.removeEventListener('loadedmetadata', onLoadedMetadata);
821
+ resolve();
822
+ };
823
+ this.element.addEventListener('loadedmetadata', onLoadedMetadata);
824
+ });
826
825
 
827
- // Create sign language video element
828
- this.signLanguageVideo = document.createElement('video');
829
- this.signLanguageVideo.className = 'vidply-sign-language-video';
830
- this.signLanguageVideo.src = this.signLanguageSrc;
831
- this.signLanguageVideo.setAttribute('aria-label', i18n.t('player.signLanguage'));
832
-
833
- // Set position based on options
834
- const position = this.options.signLanguagePosition || 'bottom-right';
835
- this.signLanguageVideo.classList.add(`vidply-sign-position-${position}`);
836
-
837
- // Sync with main video
838
- this.signLanguageVideo.muted = true; // Sign language video should be muted
839
- this.signLanguageVideo.currentTime = this.state.currentTime;
840
- if (!this.state.paused) {
841
- this.signLanguageVideo.play();
826
+ // Restore playback position
827
+ this.seek(currentTime);
828
+
829
+ if (wasPlaying) {
830
+ this.play();
831
+ }
832
+
833
+ this.state.audioDescriptionEnabled = false;
834
+ this.emit('audiodescriptiondisabled');
842
835
  }
843
836
 
844
- // Add to video wrapper (so it overlays the video, not the entire container)
845
- this.videoWrapper.appendChild(this.signLanguageVideo);
837
+ async toggleAudioDescription() {
838
+ if (this.state.audioDescriptionEnabled) {
839
+ await this.disableAudioDescription();
840
+ } else {
841
+ await this.enableAudioDescription();
842
+ }
843
+ }
846
844
 
847
- // Create bound handlers to store references for cleanup
848
- this.signLanguageHandlers = {
849
- play: () => {
850
- if (this.signLanguageVideo) {
851
- this.signLanguageVideo.play();
845
+ // Sign Language
846
+ enableSignLanguage() {
847
+ if (!this.signLanguageSrc) {
848
+ console.warn('No sign language video source provided');
849
+ return;
852
850
  }
853
- },
854
- pause: () => {
851
+
855
852
  if (this.signLanguageVideo) {
856
- this.signLanguageVideo.pause();
853
+ // Already exists, just show it
854
+ this.signLanguageVideo.style.display = 'block';
855
+ this.state.signLanguageEnabled = true;
856
+ this.emit('signlanguageenabled');
857
+ return;
857
858
  }
858
- },
859
- timeupdate: () => {
860
- if (this.signLanguageVideo && Math.abs(this.signLanguageVideo.currentTime - this.state.currentTime) > 0.5) {
861
- this.signLanguageVideo.currentTime = this.state.currentTime;
859
+
860
+ // Create sign language video element
861
+ this.signLanguageVideo = document.createElement('video');
862
+ this.signLanguageVideo.className = 'vidply-sign-language-video';
863
+ this.signLanguageVideo.src = this.signLanguageSrc;
864
+ this.signLanguageVideo.setAttribute('aria-label', i18n.t('player.signLanguage'));
865
+
866
+ // Set position based on options
867
+ const position = this.options.signLanguagePosition || 'bottom-right';
868
+ this.signLanguageVideo.classList.add(`vidply-sign-position-${position}`);
869
+
870
+ // Sync with main video
871
+ this.signLanguageVideo.muted = true; // Sign language video should be muted
872
+ this.signLanguageVideo.currentTime = this.state.currentTime;
873
+ if (!this.state.paused) {
874
+ this.signLanguageVideo.play();
862
875
  }
863
- },
864
- ratechange: () => {
876
+
877
+ // Add to video wrapper (so it overlays the video, not the entire container)
878
+ this.videoWrapper.appendChild(this.signLanguageVideo);
879
+
880
+ // Create bound handlers to store references for cleanup
881
+ this.signLanguageHandlers = {
882
+ play: () => {
883
+ if (this.signLanguageVideo) {
884
+ this.signLanguageVideo.play();
885
+ }
886
+ },
887
+ pause: () => {
888
+ if (this.signLanguageVideo) {
889
+ this.signLanguageVideo.pause();
890
+ }
891
+ },
892
+ timeupdate: () => {
893
+ if (this.signLanguageVideo && Math.abs(this.signLanguageVideo.currentTime - this.state.currentTime) > 0.5) {
894
+ this.signLanguageVideo.currentTime = this.state.currentTime;
895
+ }
896
+ },
897
+ ratechange: () => {
898
+ if (this.signLanguageVideo) {
899
+ this.signLanguageVideo.playbackRate = this.state.playbackSpeed;
900
+ }
901
+ }
902
+ };
903
+
904
+ // Sync playback
905
+ this.on('play', this.signLanguageHandlers.play);
906
+ this.on('pause', this.signLanguageHandlers.pause);
907
+ this.on('timeupdate', this.signLanguageHandlers.timeupdate);
908
+ this.on('ratechange', this.signLanguageHandlers.ratechange);
909
+
910
+ this.state.signLanguageEnabled = true;
911
+ this.emit('signlanguageenabled');
912
+ }
913
+
914
+ disableSignLanguage() {
865
915
  if (this.signLanguageVideo) {
866
- this.signLanguageVideo.playbackRate = this.state.playbackSpeed;
916
+ this.signLanguageVideo.style.display = 'none';
867
917
  }
868
- }
869
- };
870
-
871
- // Sync playback
872
- this.on('play', this.signLanguageHandlers.play);
873
- this.on('pause', this.signLanguageHandlers.pause);
874
- this.on('timeupdate', this.signLanguageHandlers.timeupdate);
875
- this.on('ratechange', this.signLanguageHandlers.ratechange);
876
-
877
- this.state.signLanguageEnabled = true;
878
- this.emit('signlanguageenabled');
879
- }
880
-
881
- disableSignLanguage() {
882
- if (this.signLanguageVideo) {
883
- this.signLanguageVideo.style.display = 'none';
918
+ this.state.signLanguageEnabled = false;
919
+ this.emit('signlanguagedisabled');
884
920
  }
885
- this.state.signLanguageEnabled = false;
886
- this.emit('signlanguagedisabled');
887
- }
888
-
889
- toggleSignLanguage() {
890
- if (this.state.signLanguageEnabled) {
891
- this.disableSignLanguage();
892
- } else {
893
- this.enableSignLanguage();
921
+
922
+ toggleSignLanguage() {
923
+ if (this.state.signLanguageEnabled) {
924
+ this.disableSignLanguage();
925
+ } else {
926
+ this.enableSignLanguage();
927
+ }
894
928
  }
895
- }
896
-
897
- cleanupSignLanguage() {
898
- // Remove event listeners
899
- if (this.signLanguageHandlers) {
900
- this.off('play', this.signLanguageHandlers.play);
901
- this.off('pause', this.signLanguageHandlers.pause);
902
- this.off('timeupdate', this.signLanguageHandlers.timeupdate);
903
- this.off('ratechange', this.signLanguageHandlers.ratechange);
904
- this.signLanguageHandlers = null;
929
+
930
+ cleanupSignLanguage() {
931
+ // Remove event listeners
932
+ if (this.signLanguageHandlers) {
933
+ this.off('play', this.signLanguageHandlers.play);
934
+ this.off('pause', this.signLanguageHandlers.pause);
935
+ this.off('timeupdate', this.signLanguageHandlers.timeupdate);
936
+ this.off('ratechange', this.signLanguageHandlers.ratechange);
937
+ this.signLanguageHandlers = null;
938
+ }
939
+
940
+ // Remove video element
941
+ if (this.signLanguageVideo && this.signLanguageVideo.parentNode) {
942
+ this.signLanguageVideo.pause();
943
+ this.signLanguageVideo.src = '';
944
+ this.signLanguageVideo.parentNode.removeChild(this.signLanguageVideo);
945
+ this.signLanguageVideo = null;
946
+ }
905
947
  }
906
948
 
907
- // Remove video element
908
- if (this.signLanguageVideo && this.signLanguageVideo.parentNode) {
909
- this.signLanguageVideo.pause();
910
- this.signLanguageVideo.src = '';
911
- this.signLanguageVideo.parentNode.removeChild(this.signLanguageVideo);
912
- this.signLanguageVideo = null;
949
+ // Settings
950
+ // Settings dialog removed - using individual control buttons instead
951
+ showSettings() {
952
+ console.warn('[VidPly] Settings dialog has been removed. Use individual control buttons (speed, captions, etc.)');
913
953
  }
914
- }
915
954
 
916
- // Settings
917
- showSettings() {
918
- if (this.settingsDialog) {
919
- this.settingsDialog.show();
955
+ hideSettings() {
956
+ // No-op - settings dialog removed
920
957
  }
921
- }
922
958
 
923
- hideSettings() {
924
- if (this.settingsDialog) {
925
- this.settingsDialog.hide();
959
+ // Utility methods
960
+ getCurrentTime() {
961
+ return this.state.currentTime;
926
962
  }
927
- }
928
963
 
929
- // Utility methods
930
- getCurrentTime() {
931
- return this.state.currentTime;
932
- }
964
+ getDuration() {
965
+ return this.state.duration;
966
+ }
933
967
 
934
- getDuration() {
935
- return this.state.duration;
936
- }
968
+ isPlaying() {
969
+ return this.state.playing;
970
+ }
937
971
 
938
- isPlaying() {
939
- return this.state.playing;
940
- }
972
+ isPaused() {
973
+ return this.state.paused;
974
+ }
941
975
 
942
- isPaused() {
943
- return this.state.paused;
944
- }
976
+ isEnded() {
977
+ return this.state.ended;
978
+ }
945
979
 
946
- isEnded() {
947
- return this.state.ended;
948
- }
980
+ isMuted() {
981
+ return this.state.muted;
982
+ }
949
983
 
950
- isMuted() {
951
- return this.state.muted;
952
- }
984
+ isFullscreen() {
985
+ return this.state.fullscreen;
986
+ }
953
987
 
954
- isFullscreen() {
955
- return this.state.fullscreen;
956
- }
988
+ // Error handling
989
+ handleError(error) {
990
+ this.log('Error:', error, 'error');
991
+ this.emit('error', error);
957
992
 
958
- // Error handling
959
- handleError(error) {
960
- this.log('Error:', error, 'error');
961
- this.emit('error', error);
962
-
963
- if (this.options.onError) {
964
- this.options.onError.call(this, error);
993
+ if (this.options.onError) {
994
+ this.options.onError.call(this, error);
995
+ }
965
996
  }
966
- }
967
997
 
968
- // Logging
969
- log(message, type = 'log') {
970
- if (this.options.debug) {
971
- console[type](`[VidPly]`, message);
972
- }
973
- }
974
-
975
- // Setup responsive handlers
976
- setupResponsiveHandlers() {
977
- // Use ResizeObserver for efficient resize tracking
978
- if (typeof ResizeObserver !== 'undefined') {
979
- this.resizeObserver = new ResizeObserver((entries) => {
980
- for (const entry of entries) {
981
- const width = entry.contentRect.width;
982
-
983
- // Update control bar for viewport
984
- if (this.controlBar && typeof this.controlBar.updateControlsForViewport === 'function') {
985
- this.controlBar.updateControlsForViewport(width);
986
- }
987
-
988
- // Update transcript positioning
989
- if (this.transcriptManager && this.transcriptManager.isVisible) {
990
- this.transcriptManager.positionTranscript();
991
- }
998
+ // Logging
999
+ log(message, type = 'log') {
1000
+ if (this.options.debug) {
1001
+ console[type](`[VidPly]`, message);
992
1002
  }
993
- });
994
-
995
- this.resizeObserver.observe(this.container);
996
- } else {
997
- // Fallback to window resize event
998
- this.resizeHandler = () => {
999
- const width = this.container.clientWidth;
1000
-
1001
- if (this.controlBar && typeof this.controlBar.updateControlsForViewport === 'function') {
1002
- this.controlBar.updateControlsForViewport(width);
1003
+ }
1004
+
1005
+ // Setup responsive handlers
1006
+ setupResponsiveHandlers() {
1007
+ // Use ResizeObserver for efficient resize tracking
1008
+ if (typeof ResizeObserver !== 'undefined') {
1009
+ this.resizeObserver = new ResizeObserver((entries) => {
1010
+ for (const entry of entries) {
1011
+ const width = entry.contentRect.width;
1012
+
1013
+ // Update control bar for viewport
1014
+ if (this.controlBar && typeof this.controlBar.updateControlsForViewport === 'function') {
1015
+ this.controlBar.updateControlsForViewport(width);
1016
+ }
1017
+
1018
+ // Update transcript positioning
1019
+ if (this.transcriptManager && this.transcriptManager.isVisible) {
1020
+ this.transcriptManager.positionTranscript();
1021
+ }
1022
+ }
1023
+ });
1024
+
1025
+ this.resizeObserver.observe(this.container);
1026
+ } else {
1027
+ // Fallback to window resize event
1028
+ this.resizeHandler = () => {
1029
+ const width = this.container.clientWidth;
1030
+
1031
+ if (this.controlBar && typeof this.controlBar.updateControlsForViewport === 'function') {
1032
+ this.controlBar.updateControlsForViewport(width);
1033
+ }
1034
+
1035
+ if (this.transcriptManager && this.transcriptManager.isVisible) {
1036
+ this.transcriptManager.positionTranscript();
1037
+ }
1038
+ };
1039
+
1040
+ window.addEventListener('resize', this.resizeHandler);
1003
1041
  }
1004
-
1005
- if (this.transcriptManager && this.transcriptManager.isVisible) {
1006
- this.transcriptManager.positionTranscript();
1042
+
1043
+ // Also listen for orientation changes on mobile
1044
+ if (window.matchMedia) {
1045
+ this.orientationHandler = (e) => {
1046
+ // Wait for layout to settle
1047
+ setTimeout(() => {
1048
+ if (this.transcriptManager && this.transcriptManager.isVisible) {
1049
+ this.transcriptManager.positionTranscript();
1050
+ }
1051
+ }, 100);
1052
+ };
1053
+
1054
+ const orientationQuery = window.matchMedia('(orientation: portrait)');
1055
+ if (orientationQuery.addEventListener) {
1056
+ orientationQuery.addEventListener('change', this.orientationHandler);
1057
+ } else if (orientationQuery.addListener) {
1058
+ // Fallback for older browsers
1059
+ orientationQuery.addListener(this.orientationHandler);
1060
+ }
1061
+
1062
+ this.orientationQuery = orientationQuery;
1007
1063
  }
1008
- };
1009
-
1010
- window.addEventListener('resize', this.resizeHandler);
1011
- }
1012
-
1013
- // Also listen for orientation changes on mobile
1014
- if (window.matchMedia) {
1015
- this.orientationHandler = (e) => {
1016
- // Wait for layout to settle
1017
- setTimeout(() => {
1018
- if (this.transcriptManager && this.transcriptManager.isVisible) {
1019
- this.transcriptManager.positionTranscript();
1020
- }
1021
- }, 100);
1022
- };
1023
-
1024
- const orientationQuery = window.matchMedia('(orientation: portrait)');
1025
- if (orientationQuery.addEventListener) {
1026
- orientationQuery.addEventListener('change', this.orientationHandler);
1027
- } else if (orientationQuery.addListener) {
1028
- // Fallback for older browsers
1029
- orientationQuery.addListener(this.orientationHandler);
1030
- }
1031
-
1032
- this.orientationQuery = orientationQuery;
1033
1064
  }
1034
- }
1035
1065
 
1036
- // Cleanup
1037
- destroy() {
1038
- this.log('Destroying player');
1039
-
1040
- if (this.renderer) {
1041
- this.renderer.destroy();
1042
- }
1043
-
1044
- if (this.controlBar) {
1045
- this.controlBar.destroy();
1046
- }
1047
-
1048
- if (this.captionManager) {
1049
- this.captionManager.destroy();
1050
- }
1051
-
1052
- if (this.keyboardManager) {
1053
- this.keyboardManager.destroy();
1054
- }
1055
-
1056
- if (this.settingsDialog) {
1057
- this.settingsDialog.destroy();
1058
- }
1059
-
1060
- if (this.transcriptManager) {
1061
- this.transcriptManager.destroy();
1062
- }
1063
-
1064
- // Cleanup sign language video and listeners
1065
- this.cleanupSignLanguage();
1066
-
1067
- // Cleanup play overlay button
1068
- if (this.playButtonOverlay && this.playButtonOverlay.parentNode) {
1069
- this.playButtonOverlay.remove();
1070
- this.playButtonOverlay = null;
1071
- }
1072
-
1073
- // Cleanup resize observer
1074
- if (this.resizeObserver) {
1075
- this.resizeObserver.disconnect();
1076
- this.resizeObserver = null;
1077
- }
1078
-
1079
- // Cleanup window resize handler
1080
- if (this.resizeHandler) {
1081
- window.removeEventListener('resize', this.resizeHandler);
1082
- this.resizeHandler = null;
1083
- }
1084
-
1085
- // Cleanup orientation change handler
1086
- if (this.orientationQuery && this.orientationHandler) {
1087
- if (this.orientationQuery.removeEventListener) {
1088
- this.orientationQuery.removeEventListener('change', this.orientationHandler);
1089
- } else if (this.orientationQuery.removeListener) {
1090
- this.orientationQuery.removeListener(this.orientationHandler);
1091
- }
1092
- this.orientationQuery = null;
1093
- this.orientationHandler = null;
1094
- }
1095
-
1096
- // Remove container
1097
- if (this.container && this.container.parentNode) {
1098
- this.container.parentNode.insertBefore(this.element, this.container);
1099
- this.container.parentNode.removeChild(this.container);
1066
+ // Cleanup
1067
+ destroy() {
1068
+ this.log('Destroying player');
1069
+
1070
+ if (this.renderer) {
1071
+ this.renderer.destroy();
1072
+ }
1073
+
1074
+ if (this.controlBar) {
1075
+ this.controlBar.destroy();
1076
+ }
1077
+
1078
+ if (this.captionManager) {
1079
+ this.captionManager.destroy();
1080
+ }
1081
+
1082
+ if (this.keyboardManager) {
1083
+ this.keyboardManager.destroy();
1084
+ }
1085
+
1086
+ if (this.transcriptManager) {
1087
+ this.transcriptManager.destroy();
1088
+ }
1089
+
1090
+ // Cleanup sign language video and listeners
1091
+ this.cleanupSignLanguage();
1092
+
1093
+ // Cleanup play overlay button
1094
+ if (this.playButtonOverlay && this.playButtonOverlay.parentNode) {
1095
+ this.playButtonOverlay.remove();
1096
+ this.playButtonOverlay = null;
1097
+ }
1098
+
1099
+ // Cleanup resize observer
1100
+ if (this.resizeObserver) {
1101
+ this.resizeObserver.disconnect();
1102
+ this.resizeObserver = null;
1103
+ }
1104
+
1105
+ // Cleanup window resize handler
1106
+ if (this.resizeHandler) {
1107
+ window.removeEventListener('resize', this.resizeHandler);
1108
+ this.resizeHandler = null;
1109
+ }
1110
+
1111
+ // Cleanup orientation change handler
1112
+ if (this.orientationQuery && this.orientationHandler) {
1113
+ if (this.orientationQuery.removeEventListener) {
1114
+ this.orientationQuery.removeEventListener('change', this.orientationHandler);
1115
+ } else if (this.orientationQuery.removeListener) {
1116
+ this.orientationQuery.removeListener(this.orientationHandler);
1117
+ }
1118
+ this.orientationQuery = null;
1119
+ this.orientationHandler = null;
1120
+ }
1121
+
1122
+ // Remove container
1123
+ if (this.container && this.container.parentNode) {
1124
+ this.container.parentNode.insertBefore(this.element, this.container);
1125
+ this.container.parentNode.removeChild(this.container);
1126
+ }
1127
+
1128
+ this.removeAllListeners();
1100
1129
  }
1101
-
1102
- this.removeAllListeners();
1103
- }
1104
1130
  }
1105
1131
 
1106
1132
  // Static instances tracker for pause others functionality