vidply 1.0.28 → 1.0.30

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.
Files changed (75) hide show
  1. package/dist/dev/vidply.HLSRenderer-UMPUDSYL.js +266 -0
  2. package/dist/dev/vidply.HLSRenderer-UMPUDSYL.js.map +7 -0
  3. package/dist/dev/vidply.HTML5Renderer-FXBZQL6Y.js +12 -0
  4. package/dist/dev/vidply.HTML5Renderer-FXBZQL6Y.js.map +7 -0
  5. package/dist/dev/{vidply.TranscriptManager-QSF2PWUN.js → vidply.TranscriptManager-T677KF4N.js} +4 -5
  6. package/dist/dev/{vidply.TranscriptManager-QSF2PWUN.js.map → vidply.TranscriptManager-T677KF4N.js.map} +2 -2
  7. package/dist/dev/{vidply.chunk-SRM7VNHG.js → vidply.chunk-GS2JX5RQ.js} +136 -95
  8. package/dist/dev/vidply.chunk-GS2JX5RQ.js.map +7 -0
  9. package/dist/dev/vidply.chunk-W2LSBD6Y.js +251 -0
  10. package/dist/dev/vidply.chunk-W2LSBD6Y.js.map +7 -0
  11. package/dist/dev/vidply.esm.js +1880 -258
  12. package/dist/dev/vidply.esm.js.map +4 -4
  13. package/dist/legacy/vidply.js +2056 -365
  14. package/dist/legacy/vidply.js.map +4 -4
  15. package/dist/legacy/vidply.min.js +1 -1
  16. package/dist/legacy/vidply.min.meta.json +111 -25
  17. package/dist/prod/vidply.HLSRenderer-3CG7BZKA.min.js +6 -0
  18. package/dist/prod/vidply.HTML5Renderer-KKW3OLHM.min.js +6 -0
  19. package/dist/prod/vidply.TranscriptManager-WFZSW6NR.min.js +6 -0
  20. package/dist/prod/vidply.chunk-34RH2THY.min.js +6 -0
  21. package/dist/prod/vidply.chunk-LGTJRPUL.min.js +6 -0
  22. package/dist/prod/vidply.esm.min.js +8 -8
  23. package/dist/vidply.css +20 -1
  24. package/dist/vidply.esm.min.meta.json +120 -34
  25. package/dist/vidply.min.css +1 -1
  26. package/package.json +2 -2
  27. package/src/controls/ControlBar.js +182 -10
  28. package/src/controls/TranscriptManager.js +7 -7
  29. package/src/core/AudioDescriptionManager.js +701 -0
  30. package/src/core/Player.js +203 -256
  31. package/src/core/SignLanguageManager.js +1134 -0
  32. package/src/renderers/HTML5Renderer.js +7 -0
  33. package/src/styles/vidply.css +20 -1
  34. package/src/utils/DOMUtils.js +153 -114
  35. package/src/utils/MenuFactory.js +374 -0
  36. package/src/utils/VideoFrameCapture.js +110 -0
  37. package/dist/dev/vidply.TranscriptManager-GZKY44ON.js +0 -1744
  38. package/dist/dev/vidply.TranscriptManager-GZKY44ON.js.map +0 -7
  39. package/dist/dev/vidply.TranscriptManager-UTJBQC5B.js +0 -1744
  40. package/dist/dev/vidply.TranscriptManager-UTJBQC5B.js.map +0 -7
  41. package/dist/dev/vidply.chunk-5663PYKK.js +0 -1631
  42. package/dist/dev/vidply.chunk-5663PYKK.js.map +0 -7
  43. package/dist/dev/vidply.chunk-SRM7VNHG.js.map +0 -7
  44. package/dist/dev/vidply.chunk-UH5MTGKF.js +0 -1630
  45. package/dist/dev/vidply.chunk-UH5MTGKF.js.map +0 -7
  46. package/dist/dev/vidply.de-RXAJM5QE.js +0 -181
  47. package/dist/dev/vidply.de-RXAJM5QE.js.map +0 -7
  48. package/dist/dev/vidply.de-THBIMP4S.js +0 -180
  49. package/dist/dev/vidply.de-THBIMP4S.js.map +0 -7
  50. package/dist/dev/vidply.es-6VWDNNNL.js +0 -180
  51. package/dist/dev/vidply.es-6VWDNNNL.js.map +0 -7
  52. package/dist/dev/vidply.es-SADVLJTQ.js +0 -181
  53. package/dist/dev/vidply.es-SADVLJTQ.js.map +0 -7
  54. package/dist/dev/vidply.fr-V3VAYBBT.js +0 -181
  55. package/dist/dev/vidply.fr-V3VAYBBT.js.map +0 -7
  56. package/dist/dev/vidply.fr-WHTWCHWT.js +0 -180
  57. package/dist/dev/vidply.fr-WHTWCHWT.js.map +0 -7
  58. package/dist/dev/vidply.ja-BFQNPOFI.js +0 -180
  59. package/dist/dev/vidply.ja-BFQNPOFI.js.map +0 -7
  60. package/dist/dev/vidply.ja-KL2TLZGJ.js +0 -181
  61. package/dist/dev/vidply.ja-KL2TLZGJ.js.map +0 -7
  62. package/dist/prod/vidply.TranscriptManager-DZ2WZU3K.min.js +0 -6
  63. package/dist/prod/vidply.TranscriptManager-E5QHGFIR.min.js +0 -6
  64. package/dist/prod/vidply.TranscriptManager-UZ6DUFB6.min.js +0 -6
  65. package/dist/prod/vidply.chunk-5DWTMWEO.min.js +0 -6
  66. package/dist/prod/vidply.chunk-IBNYTGGM.min.js +0 -6
  67. package/dist/prod/vidply.chunk-MBUR3U5L.min.js +0 -6
  68. package/dist/prod/vidply.de-HGJBCLLE.min.js +0 -6
  69. package/dist/prod/vidply.de-SWFW4HYT.min.js +0 -6
  70. package/dist/prod/vidply.es-7BJ2DJAY.min.js +0 -6
  71. package/dist/prod/vidply.es-CZEBXCZN.min.js +0 -6
  72. package/dist/prod/vidply.fr-DPVR5DFY.min.js +0 -6
  73. package/dist/prod/vidply.fr-HFOL7MWA.min.js +0 -6
  74. package/dist/prod/vidply.ja-PEBVWKVH.min.js +0 -6
  75. package/dist/prod/vidply.ja-QTVU5C25.min.js +0 -6
@@ -0,0 +1,374 @@
1
+ /**
2
+ * Menu Factory - Centralized menu creation and management
3
+ * Reduces code duplication across ControlBar menu methods
4
+ */
5
+
6
+ import { DOMUtils } from './DOMUtils.js';
7
+ import { createIconElement } from '../icons/Icons.js';
8
+ import { i18n } from '../i18n/i18n.js';
9
+ import { attachMenuKeyboardNavigation, focusFirstMenuItem } from './MenuUtils.js';
10
+
11
+ /**
12
+ * Create and show a menu with standard positioning and behavior
13
+ * @param {Object} options - Menu configuration
14
+ * @returns {HTMLElement} The created menu element
15
+ */
16
+ export function createMenu({
17
+ player,
18
+ button,
19
+ menuClass,
20
+ ariaLabel,
21
+ items = [],
22
+ activeIndex = -1,
23
+ onClose = null,
24
+ insertIntoDOM = null,
25
+ positionMenu = null,
26
+ attachCloseHandler = null
27
+ }) {
28
+ const classPrefix = player.options.classPrefix;
29
+
30
+ // Remove existing menu (toggle behavior)
31
+ const existingMenu = document.querySelector(`.${classPrefix}-${menuClass}`);
32
+ if (existingMenu) {
33
+ existingMenu.remove();
34
+ button.setAttribute('aria-expanded', 'false');
35
+ return null;
36
+ }
37
+
38
+ // Create menu container
39
+ const menu = DOMUtils.createElement('div', {
40
+ className: `${classPrefix}-${menuClass} ${classPrefix}-menu`,
41
+ attributes: {
42
+ 'role': 'menu',
43
+ 'aria-label': ariaLabel
44
+ }
45
+ });
46
+
47
+ let activeItem = null;
48
+
49
+ // Create menu items
50
+ items.forEach((itemConfig, index) => {
51
+ if (itemConfig.type === 'divider') {
52
+ const divider = DOMUtils.createElement('div', {
53
+ className: `${classPrefix}-menu-divider`,
54
+ attributes: { 'role': 'separator' }
55
+ });
56
+ menu.appendChild(divider);
57
+ return;
58
+ }
59
+
60
+ if (itemConfig.type === 'header') {
61
+ const header = DOMUtils.createElement('div', {
62
+ className: `${classPrefix}-menu-header`,
63
+ textContent: itemConfig.text
64
+ });
65
+ menu.appendChild(header);
66
+ return;
67
+ }
68
+
69
+ if (itemConfig.disabled) {
70
+ const disabledItem = DOMUtils.createElement('div', {
71
+ className: `${classPrefix}-menu-item`,
72
+ textContent: itemConfig.text,
73
+ attributes: { 'role': 'menuitem' },
74
+ style: { opacity: '0.5', cursor: 'default' }
75
+ });
76
+ menu.appendChild(disabledItem);
77
+ return;
78
+ }
79
+
80
+ const item = DOMUtils.createElement('button', {
81
+ className: `${classPrefix}-menu-item`,
82
+ attributes: {
83
+ 'type': 'button',
84
+ 'role': 'menuitem',
85
+ 'tabindex': '-1'
86
+ }
87
+ });
88
+
89
+ // Add content based on item type
90
+ if (itemConfig.icon) {
91
+ item.appendChild(createIconElement(itemConfig.icon));
92
+ }
93
+
94
+ if (itemConfig.timeLabel) {
95
+ const timeSpan = DOMUtils.createElement('span', {
96
+ className: `${classPrefix}-chapter-time`,
97
+ textContent: itemConfig.timeLabel,
98
+ attributes: itemConfig.timeAriaLabel
99
+ ? { 'aria-label': itemConfig.timeAriaLabel }
100
+ : {}
101
+ });
102
+ item.appendChild(timeSpan);
103
+ item.appendChild(document.createTextNode(' '));
104
+ }
105
+
106
+ if (itemConfig.text) {
107
+ const textSpan = DOMUtils.createElement('span', {
108
+ className: itemConfig.textClass || `${classPrefix}-menu-item-text`,
109
+ textContent: itemConfig.text
110
+ });
111
+ item.appendChild(textSpan);
112
+ }
113
+
114
+ // Mark as active
115
+ const isActive = itemConfig.active || index === activeIndex;
116
+ if (isActive) {
117
+ item.classList.add(`${classPrefix}-menu-item-active`);
118
+ item.appendChild(createIconElement('check'));
119
+ activeItem = item;
120
+ }
121
+
122
+ // Click handler
123
+ if (itemConfig.onClick) {
124
+ item.addEventListener('click', () => {
125
+ itemConfig.onClick(itemConfig.value, index);
126
+ closeMenuAndReturnFocus(menu, button, onClose);
127
+ });
128
+ }
129
+
130
+ menu.appendChild(item);
131
+ });
132
+
133
+ // Position menu (hide first to prevent jumping)
134
+ menu.style.visibility = 'hidden';
135
+ menu.style.display = 'block';
136
+
137
+ // Insert into DOM
138
+ if (insertIntoDOM) {
139
+ insertIntoDOM(menu, button);
140
+ } else {
141
+ button.insertAdjacentElement('afterend', menu);
142
+ }
143
+
144
+ // Position
145
+ if (positionMenu) {
146
+ positionMenu(menu, button, true);
147
+ }
148
+
149
+ // Show menu
150
+ requestAnimationFrame(() => {
151
+ menu.style.visibility = 'visible';
152
+ });
153
+
154
+ // Add keyboard navigation
155
+ attachMenuKeyboardNavigation(menu, button, `.${classPrefix}-menu-item`, () => {
156
+ closeMenuAndReturnFocus(menu, button, onClose);
157
+ });
158
+
159
+ // Focus active or first item
160
+ setTimeout(() => {
161
+ const focusTarget = activeItem || menu.querySelector(`.${classPrefix}-menu-item`);
162
+ if (focusTarget) {
163
+ focusTarget.focus({ preventScroll: true });
164
+ }
165
+ }, 0);
166
+
167
+ // Attach close handler
168
+ if (attachCloseHandler) {
169
+ attachCloseHandler(menu, button);
170
+ }
171
+
172
+ button.setAttribute('aria-expanded', 'true');
173
+
174
+ return menu;
175
+ }
176
+
177
+ /**
178
+ * Close menu and return focus to button
179
+ */
180
+ function closeMenuAndReturnFocus(menu, button, onClose) {
181
+ if (menu) {
182
+ menu.remove();
183
+ }
184
+ button.setAttribute('aria-expanded', 'false');
185
+ button.focus({ preventScroll: true });
186
+ if (onClose) {
187
+ onClose();
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Create a speed menu
193
+ */
194
+ export function createSpeedMenu({
195
+ player,
196
+ button,
197
+ currentSpeed,
198
+ speeds = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2],
199
+ onSpeedChange,
200
+ insertIntoDOM,
201
+ positionMenu,
202
+ attachCloseHandler
203
+ }) {
204
+ const items = speeds.map(speed => ({
205
+ text: speed === 1 ? i18n.t('player.normalSpeed') : `${speed}x`,
206
+ value: speed,
207
+ active: Math.abs(currentSpeed - speed) < 0.01,
208
+ onClick: (value) => onSpeedChange(value)
209
+ }));
210
+
211
+ return createMenu({
212
+ player,
213
+ button,
214
+ menuClass: 'speed-menu',
215
+ ariaLabel: i18n.t('player.speed'),
216
+ items,
217
+ insertIntoDOM,
218
+ positionMenu,
219
+ attachCloseHandler
220
+ });
221
+ }
222
+
223
+ /**
224
+ * Create a captions menu
225
+ */
226
+ export function createCaptionsMenu({
227
+ player,
228
+ button,
229
+ tracks,
230
+ currentTrackIndex,
231
+ captionsEnabled,
232
+ onTrackSelect,
233
+ onDisable,
234
+ insertIntoDOM,
235
+ positionMenu,
236
+ attachCloseHandler
237
+ }) {
238
+ const classPrefix = player.options.classPrefix;
239
+
240
+ const items = [
241
+ {
242
+ text: i18n.t('player.captionsOff'),
243
+ active: !captionsEnabled,
244
+ onClick: () => onDisable()
245
+ }
246
+ ];
247
+
248
+ tracks.forEach((track, index) => {
249
+ items.push({
250
+ text: track.label || track.language,
251
+ value: index,
252
+ active: captionsEnabled && currentTrackIndex === index,
253
+ onClick: () => onTrackSelect(index)
254
+ });
255
+ });
256
+
257
+ return createMenu({
258
+ player,
259
+ button,
260
+ menuClass: 'captions-menu',
261
+ ariaLabel: i18n.t('player.captions'),
262
+ items,
263
+ insertIntoDOM,
264
+ positionMenu,
265
+ attachCloseHandler
266
+ });
267
+ }
268
+
269
+ /**
270
+ * Create a chapters menu
271
+ */
272
+ export function createChaptersMenu({
273
+ player,
274
+ button,
275
+ chapters,
276
+ onChapterSelect,
277
+ formatTime,
278
+ formatDuration,
279
+ insertIntoDOM,
280
+ positionMenu,
281
+ attachCloseHandler
282
+ }) {
283
+ if (!chapters || chapters.length === 0) {
284
+ return createMenu({
285
+ player,
286
+ button,
287
+ menuClass: 'chapters-menu',
288
+ ariaLabel: i18n.t('player.chapters'),
289
+ items: [{
290
+ text: i18n.t('player.noChapters'),
291
+ disabled: true
292
+ }],
293
+ insertIntoDOM,
294
+ positionMenu,
295
+ attachCloseHandler
296
+ });
297
+ }
298
+
299
+ const items = chapters.map(chapter => ({
300
+ timeLabel: formatTime(chapter.startTime),
301
+ timeAriaLabel: formatDuration(chapter.startTime),
302
+ text: chapter.text,
303
+ textClass: `${player.options.classPrefix}-chapter-title`,
304
+ value: chapter.startTime,
305
+ onClick: (value) => onChapterSelect(value)
306
+ }));
307
+
308
+ return createMenu({
309
+ player,
310
+ button,
311
+ menuClass: 'chapters-menu',
312
+ ariaLabel: i18n.t('player.chapters'),
313
+ items,
314
+ insertIntoDOM,
315
+ positionMenu,
316
+ attachCloseHandler
317
+ });
318
+ }
319
+
320
+ /**
321
+ * Create a quality menu
322
+ */
323
+ export function createQualityMenu({
324
+ player,
325
+ button,
326
+ qualities,
327
+ currentQuality,
328
+ isHLS,
329
+ onQualitySelect,
330
+ insertIntoDOM,
331
+ positionMenu,
332
+ attachCloseHandler
333
+ }) {
334
+ const items = [];
335
+
336
+ // Auto option for HLS
337
+ if (isHLS) {
338
+ items.push({
339
+ text: i18n.t('player.auto'),
340
+ value: -1,
341
+ active: currentQuality === -1,
342
+ onClick: () => onQualitySelect(-1)
343
+ });
344
+ }
345
+
346
+ // Quality options
347
+ qualities.forEach(quality => {
348
+ items.push({
349
+ text: quality.name || `${quality.height}p`,
350
+ value: quality.index,
351
+ active: quality.index === currentQuality,
352
+ onClick: () => onQualitySelect(quality.index)
353
+ });
354
+ });
355
+
356
+ if (items.length === 0) {
357
+ items.push({
358
+ text: i18n.t('player.autoQuality'),
359
+ disabled: true
360
+ });
361
+ }
362
+
363
+ return createMenu({
364
+ player,
365
+ button,
366
+ menuClass: 'quality-menu',
367
+ ariaLabel: i18n.t('player.quality'),
368
+ items,
369
+ insertIntoDOM,
370
+ positionMenu,
371
+ attachCloseHandler
372
+ });
373
+ }
374
+
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Utility for capturing frames from video elements
3
+ */
4
+
5
+ /**
6
+ * Capture a frame from a video element at a specific time
7
+ * @param {HTMLVideoElement} video - Video element to capture from
8
+ * @param {number} time - Time in seconds to capture
9
+ * @param {Object} options - Options for frame capture
10
+ * @param {boolean} [options.restoreState=true] - Whether to restore video state after capture
11
+ * @param {number} [options.quality=0.9] - JPEG quality (0-1)
12
+ * @param {number} [options.maxWidth] - Maximum width for thumbnail
13
+ * @param {number} [options.maxHeight] - Maximum height for thumbnail
14
+ * @returns {Promise<string|null>} Data URL of the captured frame or null if failed
15
+ */
16
+ export async function captureVideoFrame(video, time, options = {}) {
17
+ if (!video || video.tagName !== 'VIDEO') {
18
+ return null;
19
+ }
20
+
21
+ const {
22
+ restoreState = true,
23
+ quality = 0.9,
24
+ maxWidth,
25
+ maxHeight
26
+ } = options;
27
+
28
+ // Save original state if we need to restore it
29
+ const wasPlaying = !video.paused;
30
+ const originalTime = video.currentTime;
31
+ const originalMuted = video.muted;
32
+
33
+ // Ensure video is muted during capture to avoid audio playback
34
+ if (restoreState) {
35
+ video.muted = true;
36
+ }
37
+
38
+ return new Promise((resolve) => {
39
+ const captureFrame = () => {
40
+ try {
41
+ // Get video dimensions
42
+ let width = video.videoWidth || 640;
43
+ let height = video.videoHeight || 360;
44
+
45
+ // Scale down if max dimensions specified
46
+ if (maxWidth && width > maxWidth) {
47
+ const ratio = maxWidth / width;
48
+ width = maxWidth;
49
+ height = Math.round(height * ratio);
50
+ }
51
+ if (maxHeight && height > maxHeight) {
52
+ const ratio = maxHeight / height;
53
+ height = maxHeight;
54
+ width = Math.round(width * ratio);
55
+ }
56
+
57
+ // Create canvas to capture frame
58
+ const canvas = document.createElement('canvas');
59
+ canvas.width = width;
60
+ canvas.height = height;
61
+
62
+ const ctx = canvas.getContext('2d');
63
+ ctx.drawImage(video, 0, 0, width, height);
64
+
65
+ const dataURL = canvas.toDataURL('image/jpeg', quality);
66
+
67
+ // Restore original state if needed
68
+ if (restoreState) {
69
+ video.currentTime = originalTime;
70
+ video.muted = originalMuted;
71
+ if (wasPlaying && !video.paused) {
72
+ video.play().catch(() => {});
73
+ }
74
+ }
75
+
76
+ resolve(dataURL);
77
+ } catch (error) {
78
+ // Restore original state on error
79
+ if (restoreState) {
80
+ video.currentTime = originalTime;
81
+ video.muted = originalMuted;
82
+ if (wasPlaying && !video.paused) {
83
+ video.play().catch(() => {});
84
+ }
85
+ }
86
+ resolve(null);
87
+ }
88
+ };
89
+
90
+ const onSeeked = () => {
91
+ video.removeEventListener('seeked', onSeeked);
92
+ // Wait for frame to be ready (double RAF for better frame quality)
93
+ requestAnimationFrame(() => {
94
+ requestAnimationFrame(captureFrame);
95
+ });
96
+ };
97
+
98
+ // Check if video is already at the right time and ready
99
+ const timeDiff = Math.abs(video.currentTime - time);
100
+ if (timeDiff < 0.1 && video.readyState >= 2) {
101
+ // Video is already at the right position, capture immediately
102
+ captureFrame();
103
+ } else {
104
+ // Seek to the desired time
105
+ video.addEventListener('seeked', onSeeked);
106
+ video.currentTime = time;
107
+ }
108
+ });
109
+ }
110
+