vidply 1.0.2 → 1.0.3

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,1094 @@
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);
292
- }
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;
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();
305
205
  }
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;
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
+ }
315
288
  }
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`);
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';
340
315
  }
341
316
 
342
- // Create video wrapper (for proper positioning of controls)
343
- this.videoWrapper = DOMUtils.createElement('div', {
344
- className: `${this.options.classPrefix}-video-wrapper`
345
- });
346
-
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;
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
+ });
364
385
  }
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;
386
+
387
+ createPlayButtonOverlay() {
388
+ // Create complete SVG play button from Icons.js
389
+ this.playButtonOverlay = createPlayOverlay();
390
+
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
+ });
370
414
  }
371
-
372
- // Set poster
373
- if (this.options.poster && this.element.tagName === 'VIDEO') {
374
- this.element.poster = this.options.poster;
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();
375
444
  }
376
-
377
- // Create centered play button overlay (only for video)
378
- if (this.element.tagName === 'VIDEO') {
379
- this.createPlayButtonOverlay();
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
+ this.emit('sourcechange', config);
520
+ this.log('Media loaded successfully');
521
+
522
+ } catch (error) {
523
+ this.handleError(error);
524
+ }
380
525
  }
381
-
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');
526
+
527
+ /**
528
+ * Check if we need to change renderer type
529
+ * @param {string} src - New source URL
530
+ * @returns {boolean}
531
+ */
532
+ shouldChangeRenderer(src) {
533
+ if (!this.renderer) return true;
534
+
535
+ const isYouTube = src.includes('youtube.com') || src.includes('youtu.be');
536
+ const isVimeo = src.includes('vimeo.com');
537
+ const isHLS = src.includes('.m3u8');
538
+
539
+ const currentRendererName = this.renderer.constructor.name;
540
+
541
+ if (isYouTube && currentRendererName !== 'YouTubeRenderer') return true;
542
+ if (isVimeo && currentRendererName !== 'VimeoRenderer') return true;
543
+ if (isHLS && currentRendererName !== 'HLSRenderer') return true;
544
+ if (!isYouTube && !isVimeo && !isHLS && currentRendererName !== 'HTML5Renderer') return true;
545
+
546
+ return false;
426
547
  }
427
548
 
428
- // Store original source for audio description toggling
429
- if (!this.originalSrc) {
430
- this.originalSrc = src;
549
+ // Playback controls
550
+ play() {
551
+ if (this.renderer) {
552
+ this.renderer.play();
553
+ }
431
554
  }
432
555
 
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;
556
+ pause() {
557
+ if (this.renderer) {
558
+ this.renderer.pause();
559
+ }
444
560
  }
445
561
 
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) {
562
+ stop() {
465
563
  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);
564
+ this.seek(0);
529
565
  }
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
-
554
- // Playback controls
555
- play() {
556
- if (this.renderer) {
557
- this.renderer.play();
558
- }
559
- }
560
566
 
561
- pause() {
562
- if (this.renderer) {
563
- this.renderer.pause();
567
+ toggle() {
568
+ if (this.state.playing) {
569
+ this.pause();
570
+ } else {
571
+ this.play();
572
+ }
564
573
  }
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();
574
+
575
+ seek(time) {
576
+ if (this.renderer) {
577
+ this.renderer.seek(time);
578
+ }
577
579
  }
578
- }
579
580
 
580
- seek(time) {
581
- if (this.renderer) {
582
- this.renderer.seek(time);
581
+ seekForward(interval = this.options.seekInterval) {
582
+ this.seek(Math.min(this.state.currentTime + interval, this.state.duration));
583
583
  }
584
- }
585
584
 
586
- seekForward(interval = this.options.seekInterval) {
587
- this.seek(Math.min(this.state.currentTime + interval, this.state.duration));
588
- }
585
+ seekBackward(interval = this.options.seekInterval) {
586
+ this.seek(Math.max(this.state.currentTime - interval, 0));
587
+ }
589
588
 
590
- seekBackward(interval = this.options.seekInterval) {
591
- this.seek(Math.max(this.state.currentTime - interval, 0));
592
- }
589
+ // Volume controls
590
+ setVolume(volume) {
591
+ const newVolume = Math.max(0, Math.min(1, volume));
592
+ if (this.renderer) {
593
+ this.renderer.setVolume(newVolume);
594
+ }
595
+ this.state.volume = newVolume;
593
596
 
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);
599
- }
600
- this.state.volume = newVolume;
601
-
602
- if (newVolume > 0 && this.state.muted) {
603
- this.state.muted = false;
597
+ if (newVolume > 0 && this.state.muted) {
598
+ this.state.muted = false;
599
+ }
604
600
  }
605
- }
606
-
607
- getVolume() {
608
- return this.state.volume;
609
- }
610
601
 
611
- mute() {
612
- if (this.renderer) {
613
- this.renderer.setMuted(true);
602
+ getVolume() {
603
+ return this.state.volume;
614
604
  }
615
- this.state.muted = true;
616
- }
617
605
 
618
- unmute() {
619
- if (this.renderer) {
620
- this.renderer.setMuted(false);
606
+ mute() {
607
+ if (this.renderer) {
608
+ this.renderer.setMuted(true);
609
+ }
610
+ this.state.muted = true;
611
+ this.emit('volumechange');
621
612
  }
622
- this.state.muted = false;
623
- }
624
-
625
- toggleMute() {
626
- if (this.state.muted) {
627
- this.unmute();
628
- } else {
629
- this.mute();
613
+
614
+ unmute() {
615
+ if (this.renderer) {
616
+ this.renderer.setMuted(false);
617
+ }
618
+ this.state.muted = false;
619
+ this.emit('volumechange');
630
620
  }
631
- }
632
621
 
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);
622
+ toggleMute() {
623
+ if (this.state.muted) {
624
+ this.unmute();
625
+ } else {
626
+ this.mute();
627
+ }
638
628
  }
639
- this.state.playbackSpeed = newSpeed;
640
- this.emit('playbackspeedchange', newSpeed);
641
- }
642
-
643
- getPlaybackSpeed() {
644
- return this.state.playbackSpeed;
645
- }
646
-
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();
629
+
630
+ // Playback speed
631
+ setPlaybackSpeed(speed) {
632
+ const newSpeed = Math.max(0.25, Math.min(2, speed));
633
+ if (this.renderer) {
634
+ this.renderer.setPlaybackSpeed(newSpeed);
635
+ }
636
+ this.state.playbackSpeed = newSpeed;
637
+ this.emit('playbackspeedchange', newSpeed);
659
638
  }
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();
639
+
640
+ getPlaybackSpeed() {
641
+ return this.state.playbackSpeed;
675
642
  }
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();
643
+
644
+ // Fullscreen
645
+ enterFullscreen() {
646
+ const elem = this.container;
647
+
648
+ if (elem.requestFullscreen) {
649
+ elem.requestFullscreen();
650
+ } else if (elem.webkitRequestFullscreen) {
651
+ elem.webkitRequestFullscreen();
652
+ } else if (elem.mozRequestFullScreen) {
653
+ elem.mozRequestFullScreen();
654
+ } else if (elem.msRequestFullscreen) {
655
+ elem.msRequestFullscreen();
656
+ }
657
+
658
+ this.state.fullscreen = true;
659
+ this.container.classList.add(`${this.options.classPrefix}-fullscreen`);
660
+ this.emit('fullscreenchange', true);
687
661
  }
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);
662
+
663
+ exitFullscreen() {
664
+ if (document.exitFullscreen) {
665
+ document.exitFullscreen();
666
+ } else if (document.webkitExitFullscreen) {
667
+ document.webkitExitFullscreen();
668
+ } else if (document.mozCancelFullScreen) {
669
+ document.mozCancelFullScreen();
670
+ } else if (document.msExitFullscreen) {
671
+ document.msExitFullscreen();
672
+ }
673
+
674
+ this.state.fullscreen = false;
675
+ this.container.classList.remove(`${this.options.classPrefix}-fullscreen`);
676
+ this.emit('fullscreenchange', false);
696
677
  }
697
- }
698
678
 
699
- exitPiP() {
700
- if (document.pictureInPictureElement) {
701
- document.exitPictureInPicture();
702
- this.state.pip = false;
703
- this.emit('pipchange', false);
679
+ toggleFullscreen() {
680
+ if (this.state.fullscreen) {
681
+ this.exitFullscreen();
682
+ } else {
683
+ this.enterFullscreen();
684
+ }
704
685
  }
705
- }
706
686
 
707
- togglePiP() {
708
- if (this.state.pip) {
709
- this.exitPiP();
710
- } else {
711
- this.enterPiP();
687
+ // Picture-in-Picture
688
+ enterPiP() {
689
+ if (this.element.requestPictureInPicture) {
690
+ this.element.requestPictureInPicture();
691
+ this.state.pip = true;
692
+ this.emit('pipchange', true);
693
+ }
712
694
  }
713
- }
714
695
 
715
- // Captions
716
- enableCaptions() {
717
- if (this.captionManager) {
718
- this.captionManager.enable();
719
- this.state.captionsEnabled = true;
696
+ exitPiP() {
697
+ if (document.pictureInPictureElement) {
698
+ document.exitPictureInPicture();
699
+ this.state.pip = false;
700
+ this.emit('pipchange', false);
701
+ }
720
702
  }
721
- }
722
703
 
723
- disableCaptions() {
724
- if (this.captionManager) {
725
- this.captionManager.disable();
726
- this.state.captionsEnabled = false;
704
+ togglePiP() {
705
+ if (this.state.pip) {
706
+ this.exitPiP();
707
+ } else {
708
+ this.enterPiP();
709
+ }
727
710
  }
728
- }
729
711
 
730
- toggleCaptions() {
731
- if (this.state.captionsEnabled) {
732
- this.disableCaptions();
733
- } else {
734
- this.enableCaptions();
712
+ // Captions
713
+ enableCaptions() {
714
+ if (this.captionManager) {
715
+ this.captionManager.enable();
716
+ this.state.captionsEnabled = true;
717
+ }
735
718
  }
736
- }
737
719
 
738
- // Audio Description
739
- async enableAudioDescription() {
740
- if (!this.audioDescriptionSrc) {
741
- console.warn('VidPly: No audio description source provided');
742
- return;
720
+ disableCaptions() {
721
+ if (this.captionManager) {
722
+ this.captionManager.disable();
723
+ this.state.captionsEnabled = false;
724
+ }
743
725
  }
744
726
 
745
- // Store current playback state
746
- const currentTime = this.state.currentTime;
747
- const wasPlaying = this.state.playing;
748
-
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();
727
+ toggleCaptions() {
728
+ if (this.state.captionsEnabled) {
729
+ this.disableCaptions();
730
+ } else {
731
+ this.enableCaptions();
732
+ }
766
733
  }
767
734
 
768
- this.state.audioDescriptionEnabled = true;
769
- this.emit('audiodescriptionenabled');
770
- }
735
+ // Audio Description
736
+ async enableAudioDescription() {
737
+ if (!this.audioDescriptionSrc) {
738
+ console.warn('VidPly: No audio description source provided');
739
+ return;
740
+ }
771
741
 
772
- async disableAudioDescription() {
773
- if (!this.originalSrc) {
774
- return;
775
- }
742
+ // Store current playback state
743
+ const currentTime = this.state.currentTime;
744
+ const wasPlaying = this.state.playing;
776
745
 
777
- // Store current playback state
778
- const currentTime = this.state.currentTime;
779
- const wasPlaying = this.state.playing;
780
-
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();
798
- }
746
+ // Switch to audio-described version
747
+ this.element.src = this.audioDescriptionSrc;
799
748
 
800
- this.state.audioDescriptionEnabled = false;
801
- this.emit('audiodescriptiondisabled');
802
- }
749
+ // Wait for new source to load
750
+ await new Promise((resolve) => {
751
+ const onLoadedMetadata = () => {
752
+ this.element.removeEventListener('loadedmetadata', onLoadedMetadata);
753
+ resolve();
754
+ };
755
+ this.element.addEventListener('loadedmetadata', onLoadedMetadata);
756
+ });
803
757
 
804
- async toggleAudioDescription() {
805
- if (this.state.audioDescriptionEnabled) {
806
- await this.disableAudioDescription();
807
- } else {
808
- await this.enableAudioDescription();
809
- }
810
- }
758
+ // Restore playback position
759
+ this.seek(currentTime);
811
760
 
812
- // Sign Language
813
- enableSignLanguage() {
814
- if (!this.signLanguageSrc) {
815
- console.warn('No sign language video source provided');
816
- return;
817
- }
761
+ if (wasPlaying) {
762
+ this.play();
763
+ }
818
764
 
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;
765
+ this.state.audioDescriptionEnabled = true;
766
+ this.emit('audiodescriptionenabled');
825
767
  }
826
768
 
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();
842
- }
769
+ async disableAudioDescription() {
770
+ if (!this.originalSrc) {
771
+ return;
772
+ }
843
773
 
844
- // Add to video wrapper (so it overlays the video, not the entire container)
845
- this.videoWrapper.appendChild(this.signLanguageVideo);
774
+ // Store current playback state
775
+ const currentTime = this.state.currentTime;
776
+ const wasPlaying = this.state.playing;
846
777
 
847
- // Create bound handlers to store references for cleanup
848
- this.signLanguageHandlers = {
849
- play: () => {
850
- if (this.signLanguageVideo) {
851
- this.signLanguageVideo.play();
778
+ // Switch back to original version
779
+ this.element.src = this.originalSrc;
780
+
781
+ // Wait for new source to load
782
+ await new Promise((resolve) => {
783
+ const onLoadedMetadata = () => {
784
+ this.element.removeEventListener('loadedmetadata', onLoadedMetadata);
785
+ resolve();
786
+ };
787
+ this.element.addEventListener('loadedmetadata', onLoadedMetadata);
788
+ });
789
+
790
+ // Restore playback position
791
+ this.seek(currentTime);
792
+
793
+ if (wasPlaying) {
794
+ this.play();
852
795
  }
853
- },
854
- pause: () => {
855
- if (this.signLanguageVideo) {
856
- this.signLanguageVideo.pause();
796
+
797
+ this.state.audioDescriptionEnabled = false;
798
+ this.emit('audiodescriptiondisabled');
799
+ }
800
+
801
+ async toggleAudioDescription() {
802
+ if (this.state.audioDescriptionEnabled) {
803
+ await this.disableAudioDescription();
804
+ } else {
805
+ await this.enableAudioDescription();
857
806
  }
858
- },
859
- timeupdate: () => {
860
- if (this.signLanguageVideo && Math.abs(this.signLanguageVideo.currentTime - this.state.currentTime) > 0.5) {
861
- this.signLanguageVideo.currentTime = this.state.currentTime;
807
+ }
808
+
809
+ // Sign Language
810
+ enableSignLanguage() {
811
+ if (!this.signLanguageSrc) {
812
+ console.warn('No sign language video source provided');
813
+ return;
862
814
  }
863
- },
864
- ratechange: () => {
815
+
865
816
  if (this.signLanguageVideo) {
866
- this.signLanguageVideo.playbackRate = this.state.playbackSpeed;
817
+ // Already exists, just show it
818
+ this.signLanguageVideo.style.display = 'block';
819
+ this.state.signLanguageEnabled = true;
820
+ this.emit('signlanguageenabled');
821
+ return;
867
822
  }
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';
884
- }
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();
894
- }
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;
905
- }
906
823
 
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;
913
- }
914
- }
824
+ // Create sign language video element
825
+ this.signLanguageVideo = document.createElement('video');
826
+ this.signLanguageVideo.className = 'vidply-sign-language-video';
827
+ this.signLanguageVideo.src = this.signLanguageSrc;
828
+ this.signLanguageVideo.setAttribute('aria-label', i18n.t('player.signLanguage'));
829
+
830
+ // Set position based on options
831
+ const position = this.options.signLanguagePosition || 'bottom-right';
832
+ this.signLanguageVideo.classList.add(`vidply-sign-position-${position}`);
833
+
834
+ // Sync with main video
835
+ this.signLanguageVideo.muted = true; // Sign language video should be muted
836
+ this.signLanguageVideo.currentTime = this.state.currentTime;
837
+ if (!this.state.paused) {
838
+ this.signLanguageVideo.play();
839
+ }
915
840
 
916
- // Settings
917
- showSettings() {
918
- if (this.settingsDialog) {
919
- this.settingsDialog.show();
841
+ // Add to video wrapper (so it overlays the video, not the entire container)
842
+ this.videoWrapper.appendChild(this.signLanguageVideo);
843
+
844
+ // Create bound handlers to store references for cleanup
845
+ this.signLanguageHandlers = {
846
+ play: () => {
847
+ if (this.signLanguageVideo) {
848
+ this.signLanguageVideo.play();
849
+ }
850
+ },
851
+ pause: () => {
852
+ if (this.signLanguageVideo) {
853
+ this.signLanguageVideo.pause();
854
+ }
855
+ },
856
+ timeupdate: () => {
857
+ if (this.signLanguageVideo && Math.abs(this.signLanguageVideo.currentTime - this.state.currentTime) > 0.5) {
858
+ this.signLanguageVideo.currentTime = this.state.currentTime;
859
+ }
860
+ },
861
+ ratechange: () => {
862
+ if (this.signLanguageVideo) {
863
+ this.signLanguageVideo.playbackRate = this.state.playbackSpeed;
864
+ }
865
+ }
866
+ };
867
+
868
+ // Sync playback
869
+ this.on('play', this.signLanguageHandlers.play);
870
+ this.on('pause', this.signLanguageHandlers.pause);
871
+ this.on('timeupdate', this.signLanguageHandlers.timeupdate);
872
+ this.on('ratechange', this.signLanguageHandlers.ratechange);
873
+
874
+ this.state.signLanguageEnabled = true;
875
+ this.emit('signlanguageenabled');
920
876
  }
921
- }
922
877
 
923
- hideSettings() {
924
- if (this.settingsDialog) {
925
- this.settingsDialog.hide();
926
- }
927
- }
928
-
929
- // Utility methods
930
- getCurrentTime() {
931
- return this.state.currentTime;
932
- }
933
-
934
- getDuration() {
935
- return this.state.duration;
936
- }
937
-
938
- isPlaying() {
939
- return this.state.playing;
940
- }
941
-
942
- isPaused() {
943
- return this.state.paused;
944
- }
945
-
946
- isEnded() {
947
- return this.state.ended;
948
- }
949
-
950
- isMuted() {
951
- return this.state.muted;
952
- }
953
-
954
- isFullscreen() {
955
- return this.state.fullscreen;
956
- }
957
-
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);
878
+ disableSignLanguage() {
879
+ if (this.signLanguageVideo) {
880
+ this.signLanguageVideo.style.display = 'none';
881
+ }
882
+ this.state.signLanguageEnabled = false;
883
+ this.emit('signlanguagedisabled');
965
884
  }
966
- }
967
885
 
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
- }
886
+ toggleSignLanguage() {
887
+ if (this.state.signLanguageEnabled) {
888
+ this.disableSignLanguage();
889
+ } else {
890
+ this.enableSignLanguage();
992
891
  }
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);
892
+ }
893
+
894
+ cleanupSignLanguage() {
895
+ // Remove event listeners
896
+ if (this.signLanguageHandlers) {
897
+ this.off('play', this.signLanguageHandlers.play);
898
+ this.off('pause', this.signLanguageHandlers.pause);
899
+ this.off('timeupdate', this.signLanguageHandlers.timeupdate);
900
+ this.off('ratechange', this.signLanguageHandlers.ratechange);
901
+ this.signLanguageHandlers = null;
1003
902
  }
1004
-
1005
- if (this.transcriptManager && this.transcriptManager.isVisible) {
1006
- this.transcriptManager.positionTranscript();
903
+
904
+ // Remove video element
905
+ if (this.signLanguageVideo && this.signLanguageVideo.parentNode) {
906
+ this.signLanguageVideo.pause();
907
+ this.signLanguageVideo.src = '';
908
+ this.signLanguageVideo.parentNode.removeChild(this.signLanguageVideo);
909
+ this.signLanguageVideo = null;
1007
910
  }
1008
- };
1009
-
1010
- window.addEventListener('resize', this.resizeHandler);
1011
911
  }
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;
912
+
913
+ // Settings
914
+ // Settings dialog removed - using individual control buttons instead
915
+ showSettings() {
916
+ console.warn('[VidPly] Settings dialog has been removed. Use individual control buttons (speed, captions, etc.)');
917
+ }
918
+
919
+ hideSettings() {
920
+ // No-op - settings dialog removed
1033
921
  }
1034
- }
1035
-
1036
- // Cleanup
1037
- destroy() {
1038
- this.log('Destroying player');
1039
-
1040
- if (this.renderer) {
1041
- this.renderer.destroy();
922
+
923
+ // Utility methods
924
+ getCurrentTime() {
925
+ return this.state.currentTime;
1042
926
  }
1043
-
1044
- if (this.controlBar) {
1045
- this.controlBar.destroy();
927
+
928
+ getDuration() {
929
+ return this.state.duration;
1046
930
  }
1047
-
1048
- if (this.captionManager) {
1049
- this.captionManager.destroy();
931
+
932
+ isPlaying() {
933
+ return this.state.playing;
1050
934
  }
1051
-
1052
- if (this.keyboardManager) {
1053
- this.keyboardManager.destroy();
935
+
936
+ isPaused() {
937
+ return this.state.paused;
1054
938
  }
1055
-
1056
- if (this.settingsDialog) {
1057
- this.settingsDialog.destroy();
939
+
940
+ isEnded() {
941
+ return this.state.ended;
1058
942
  }
1059
-
1060
- if (this.transcriptManager) {
1061
- this.transcriptManager.destroy();
943
+
944
+ isMuted() {
945
+ return this.state.muted;
1062
946
  }
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;
947
+
948
+ isFullscreen() {
949
+ return this.state.fullscreen;
1071
950
  }
1072
-
1073
- // Cleanup resize observer
1074
- if (this.resizeObserver) {
1075
- this.resizeObserver.disconnect();
1076
- this.resizeObserver = null;
951
+
952
+ // Error handling
953
+ handleError(error) {
954
+ this.log('Error:', error, 'error');
955
+ this.emit('error', error);
956
+
957
+ if (this.options.onError) {
958
+ this.options.onError.call(this, error);
959
+ }
1077
960
  }
1078
-
1079
- // Cleanup window resize handler
1080
- if (this.resizeHandler) {
1081
- window.removeEventListener('resize', this.resizeHandler);
1082
- this.resizeHandler = null;
961
+
962
+ // Logging
963
+ log(message, type = 'log') {
964
+ if (this.options.debug) {
965
+ console[type](`[VidPly]`, message);
966
+ }
1083
967
  }
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;
968
+
969
+ // Setup responsive handlers
970
+ setupResponsiveHandlers() {
971
+ // Use ResizeObserver for efficient resize tracking
972
+ if (typeof ResizeObserver !== 'undefined') {
973
+ this.resizeObserver = new ResizeObserver((entries) => {
974
+ for (const entry of entries) {
975
+ const width = entry.contentRect.width;
976
+
977
+ // Update control bar for viewport
978
+ if (this.controlBar && typeof this.controlBar.updateControlsForViewport === 'function') {
979
+ this.controlBar.updateControlsForViewport(width);
980
+ }
981
+
982
+ // Update transcript positioning
983
+ if (this.transcriptManager && this.transcriptManager.isVisible) {
984
+ this.transcriptManager.positionTranscript();
985
+ }
986
+ }
987
+ });
988
+
989
+ this.resizeObserver.observe(this.container);
990
+ } else {
991
+ // Fallback to window resize event
992
+ this.resizeHandler = () => {
993
+ const width = this.container.clientWidth;
994
+
995
+ if (this.controlBar && typeof this.controlBar.updateControlsForViewport === 'function') {
996
+ this.controlBar.updateControlsForViewport(width);
997
+ }
998
+
999
+ if (this.transcriptManager && this.transcriptManager.isVisible) {
1000
+ this.transcriptManager.positionTranscript();
1001
+ }
1002
+ };
1003
+
1004
+ window.addEventListener('resize', this.resizeHandler);
1005
+ }
1006
+
1007
+ // Also listen for orientation changes on mobile
1008
+ if (window.matchMedia) {
1009
+ this.orientationHandler = (e) => {
1010
+ // Wait for layout to settle
1011
+ setTimeout(() => {
1012
+ if (this.transcriptManager && this.transcriptManager.isVisible) {
1013
+ this.transcriptManager.positionTranscript();
1014
+ }
1015
+ }, 100);
1016
+ };
1017
+
1018
+ const orientationQuery = window.matchMedia('(orientation: portrait)');
1019
+ if (orientationQuery.addEventListener) {
1020
+ orientationQuery.addEventListener('change', this.orientationHandler);
1021
+ } else if (orientationQuery.addListener) {
1022
+ // Fallback for older browsers
1023
+ orientationQuery.addListener(this.orientationHandler);
1024
+ }
1025
+
1026
+ this.orientationQuery = orientationQuery;
1027
+ }
1094
1028
  }
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);
1029
+
1030
+ // Cleanup
1031
+ destroy() {
1032
+ this.log('Destroying player');
1033
+
1034
+ if (this.renderer) {
1035
+ this.renderer.destroy();
1036
+ }
1037
+
1038
+ if (this.controlBar) {
1039
+ this.controlBar.destroy();
1040
+ }
1041
+
1042
+ if (this.captionManager) {
1043
+ this.captionManager.destroy();
1044
+ }
1045
+
1046
+ if (this.keyboardManager) {
1047
+ this.keyboardManager.destroy();
1048
+ }
1049
+
1050
+ if (this.transcriptManager) {
1051
+ this.transcriptManager.destroy();
1052
+ }
1053
+
1054
+ // Cleanup sign language video and listeners
1055
+ this.cleanupSignLanguage();
1056
+
1057
+ // Cleanup play overlay button
1058
+ if (this.playButtonOverlay && this.playButtonOverlay.parentNode) {
1059
+ this.playButtonOverlay.remove();
1060
+ this.playButtonOverlay = null;
1061
+ }
1062
+
1063
+ // Cleanup resize observer
1064
+ if (this.resizeObserver) {
1065
+ this.resizeObserver.disconnect();
1066
+ this.resizeObserver = null;
1067
+ }
1068
+
1069
+ // Cleanup window resize handler
1070
+ if (this.resizeHandler) {
1071
+ window.removeEventListener('resize', this.resizeHandler);
1072
+ this.resizeHandler = null;
1073
+ }
1074
+
1075
+ // Cleanup orientation change handler
1076
+ if (this.orientationQuery && this.orientationHandler) {
1077
+ if (this.orientationQuery.removeEventListener) {
1078
+ this.orientationQuery.removeEventListener('change', this.orientationHandler);
1079
+ } else if (this.orientationQuery.removeListener) {
1080
+ this.orientationQuery.removeListener(this.orientationHandler);
1081
+ }
1082
+ this.orientationQuery = null;
1083
+ this.orientationHandler = null;
1084
+ }
1085
+
1086
+ // Remove container
1087
+ if (this.container && this.container.parentNode) {
1088
+ this.container.parentNode.insertBefore(this.element, this.container);
1089
+ this.container.parentNode.removeChild(this.container);
1090
+ }
1091
+
1092
+ this.removeAllListeners();
1100
1093
  }
1101
-
1102
- this.removeAllListeners();
1103
- }
1104
1094
  }
1105
1095
 
1106
1096
  // Static instances tracker for pause others functionality