vidply 1.0.8 → 1.0.9
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.
- package/dist/vidply.css +113 -44
- package/dist/vidply.esm.js +806 -70
- package/dist/vidply.esm.js.map +2 -2
- package/dist/vidply.esm.min.js +3 -3
- package/dist/vidply.esm.min.meta.json +7 -7
- package/dist/vidply.js +806 -70
- package/dist/vidply.js.map +2 -2
- package/dist/vidply.min.css +1 -1
- package/dist/vidply.min.js +3 -3
- package/dist/vidply.min.meta.json +7 -7
- package/package.json +1 -1
- package/src/controls/TranscriptManager.js +328 -27
- package/src/core/Player.js +682 -56
- package/src/i18n/translations.js +10 -5
- package/src/icons/Icons.js +2 -2
- package/src/styles/vidply.css +113 -44
package/src/core/Player.js
CHANGED
|
@@ -143,6 +143,8 @@ export class Player extends EventEmitter {
|
|
|
143
143
|
screenReaderAnnouncements: true,
|
|
144
144
|
highContrast: false,
|
|
145
145
|
focusHighlight: true,
|
|
146
|
+
metadataAlerts: {},
|
|
147
|
+
metadataHashtags: {},
|
|
146
148
|
|
|
147
149
|
// Languages
|
|
148
150
|
language: 'en',
|
|
@@ -166,6 +168,9 @@ export class Player extends EventEmitter {
|
|
|
166
168
|
...options
|
|
167
169
|
};
|
|
168
170
|
|
|
171
|
+
this.options.metadataAlerts = this.options.metadataAlerts || {};
|
|
172
|
+
this.options.metadataHashtags = this.options.metadataHashtags || {};
|
|
173
|
+
|
|
169
174
|
// Storage manager
|
|
170
175
|
this.storage = new StorageManager('vidply');
|
|
171
176
|
|
|
@@ -210,6 +215,17 @@ export class Player extends EventEmitter {
|
|
|
210
215
|
// Store caption tracks that should be swapped for audio description
|
|
211
216
|
this.audioDescriptionCaptionTracks = [];
|
|
212
217
|
|
|
218
|
+
// DOM query cache (for performance optimization)
|
|
219
|
+
this._textTracksCache = null;
|
|
220
|
+
this._textTracksDirty = true;
|
|
221
|
+
this._sourceElementsCache = null;
|
|
222
|
+
this._sourceElementsDirty = true;
|
|
223
|
+
this._trackElementsCache = null;
|
|
224
|
+
this._trackElementsDirty = true;
|
|
225
|
+
|
|
226
|
+
// Timeout management (for cleanup)
|
|
227
|
+
this.timeouts = new Set();
|
|
228
|
+
|
|
213
229
|
// Components
|
|
214
230
|
this.container = null;
|
|
215
231
|
this.renderer = null;
|
|
@@ -217,6 +233,10 @@ export class Player extends EventEmitter {
|
|
|
217
233
|
this.captionManager = null;
|
|
218
234
|
this.keyboardManager = null;
|
|
219
235
|
this.settingsDialog = null;
|
|
236
|
+
|
|
237
|
+
// Metadata handling
|
|
238
|
+
this.metadataCueChangeHandler = null;
|
|
239
|
+
this.metadataAlertHandlers = new Map();
|
|
220
240
|
|
|
221
241
|
// Initialize
|
|
222
242
|
this.init();
|
|
@@ -264,6 +284,9 @@ export class Player extends EventEmitter {
|
|
|
264
284
|
if (this.options.transcript || this.options.transcriptButton) {
|
|
265
285
|
this.transcriptManager = new TranscriptManager(this);
|
|
266
286
|
}
|
|
287
|
+
|
|
288
|
+
// Always set up metadata track handling (independent of transcript)
|
|
289
|
+
this.setupMetadataHandling();
|
|
267
290
|
|
|
268
291
|
// Initialize keyboard controls
|
|
269
292
|
if (this.options.keyboard) {
|
|
@@ -399,6 +422,12 @@ export class Player extends EventEmitter {
|
|
|
399
422
|
if (this.element.tagName === 'VIDEO') {
|
|
400
423
|
this.createPlayButtonOverlay();
|
|
401
424
|
}
|
|
425
|
+
|
|
426
|
+
// Store reference to player on element for easy access
|
|
427
|
+
this.element.vidply = this;
|
|
428
|
+
|
|
429
|
+
// Add to static instances array
|
|
430
|
+
Player.instances.push(this);
|
|
402
431
|
|
|
403
432
|
// Make video/audio element clickable to toggle play/pause
|
|
404
433
|
this.element.style.cursor = 'pointer';
|
|
@@ -447,7 +476,7 @@ export class Player extends EventEmitter {
|
|
|
447
476
|
}
|
|
448
477
|
|
|
449
478
|
// Check for source elements with audio description attributes
|
|
450
|
-
const sourceElements = this.
|
|
479
|
+
const sourceElements = this.sourceElements;
|
|
451
480
|
for (const sourceEl of sourceElements) {
|
|
452
481
|
const descSrc = sourceEl.getAttribute('data-desc-src');
|
|
453
482
|
const origSrc = sourceEl.getAttribute('data-orig-src');
|
|
@@ -490,7 +519,7 @@ export class Player extends EventEmitter {
|
|
|
490
519
|
// Check for caption/subtitle tracks with audio description versions
|
|
491
520
|
// Only tracks with explicit data-desc-src attribute are swapped (no auto-detection to avoid 404 errors)
|
|
492
521
|
// Description tracks (kind="descriptions") are NOT swapped - they're for transcripts
|
|
493
|
-
const trackElements = this.
|
|
522
|
+
const trackElements = this.trackElements;
|
|
494
523
|
trackElements.forEach(trackEl => {
|
|
495
524
|
const trackKind = trackEl.getAttribute('kind');
|
|
496
525
|
const trackDescSrc = trackEl.getAttribute('data-desc-src');
|
|
@@ -537,6 +566,117 @@ export class Player extends EventEmitter {
|
|
|
537
566
|
this.log(`Using ${renderer.name} renderer`);
|
|
538
567
|
this.renderer = new renderer(this);
|
|
539
568
|
await this.renderer.init();
|
|
569
|
+
|
|
570
|
+
// Invalidate cache after renderer initialization (tracks may have changed)
|
|
571
|
+
this.invalidateTrackCache();
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Get cached text tracks array
|
|
576
|
+
* @returns {Array} Array of text tracks
|
|
577
|
+
*/
|
|
578
|
+
get textTracks() {
|
|
579
|
+
if (!this._textTracksCache || this._textTracksDirty) {
|
|
580
|
+
this._textTracksCache = Array.from(this.element.textTracks || []);
|
|
581
|
+
this._textTracksDirty = false;
|
|
582
|
+
}
|
|
583
|
+
return this._textTracksCache;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Get cached source elements array
|
|
588
|
+
* @returns {Array} Array of source elements
|
|
589
|
+
*/
|
|
590
|
+
get sourceElements() {
|
|
591
|
+
if (!this._sourceElementsCache || this._sourceElementsDirty) {
|
|
592
|
+
this._sourceElementsCache = Array.from(this.element.querySelectorAll('source'));
|
|
593
|
+
this._sourceElementsDirty = false;
|
|
594
|
+
}
|
|
595
|
+
return this._sourceElementsCache;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Get cached track elements array
|
|
600
|
+
* @returns {Array} Array of track elements
|
|
601
|
+
*/
|
|
602
|
+
get trackElements() {
|
|
603
|
+
if (!this._trackElementsCache || this._trackElementsDirty) {
|
|
604
|
+
this._trackElementsCache = Array.from(this.element.querySelectorAll('track'));
|
|
605
|
+
this._trackElementsDirty = false;
|
|
606
|
+
}
|
|
607
|
+
return this._trackElementsCache;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Invalidate DOM query cache (call when tracks/sources change)
|
|
612
|
+
*/
|
|
613
|
+
invalidateTrackCache() {
|
|
614
|
+
this._textTracksDirty = true;
|
|
615
|
+
this._trackElementsDirty = true;
|
|
616
|
+
this._sourceElementsDirty = true;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Find a text track by kind and optionally language
|
|
621
|
+
* @param {string} kind - Track kind (captions, subtitles, descriptions, chapters, metadata)
|
|
622
|
+
* @param {string} [language] - Optional language code
|
|
623
|
+
* @returns {TextTrack|null} Found track or null
|
|
624
|
+
*/
|
|
625
|
+
findTextTrack(kind, language = null) {
|
|
626
|
+
const tracks = this.textTracks;
|
|
627
|
+
if (language) {
|
|
628
|
+
return tracks.find(t => t.kind === kind && t.language === language);
|
|
629
|
+
}
|
|
630
|
+
return tracks.find(t => t.kind === kind);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Find a source element by attribute
|
|
635
|
+
* @param {string} attribute - Attribute name (e.g., 'data-desc-src')
|
|
636
|
+
* @param {string} [value] - Optional attribute value
|
|
637
|
+
* @returns {Element|null} Found source element or null
|
|
638
|
+
*/
|
|
639
|
+
findSourceElement(attribute, value = null) {
|
|
640
|
+
const sources = this.sourceElements;
|
|
641
|
+
if (value) {
|
|
642
|
+
return sources.find(el => el.getAttribute(attribute) === value);
|
|
643
|
+
}
|
|
644
|
+
return sources.find(el => el.hasAttribute(attribute));
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Find a track element by its associated TextTrack
|
|
649
|
+
* @param {TextTrack} track - The TextTrack object
|
|
650
|
+
* @returns {Element|null} Found track element or null
|
|
651
|
+
*/
|
|
652
|
+
findTrackElement(track) {
|
|
653
|
+
return this.trackElements.find(el => el.track === track);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Set a managed timeout that will be cleaned up on destroy
|
|
658
|
+
* @param {Function} callback - Callback function
|
|
659
|
+
* @param {number} delay - Delay in milliseconds
|
|
660
|
+
* @returns {number} Timeout ID
|
|
661
|
+
*/
|
|
662
|
+
setManagedTimeout(callback, delay) {
|
|
663
|
+
const timeoutId = setTimeout(() => {
|
|
664
|
+
this.timeouts.delete(timeoutId);
|
|
665
|
+
callback();
|
|
666
|
+
}, delay);
|
|
667
|
+
this.timeouts.add(timeoutId);
|
|
668
|
+
return timeoutId;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Clear a managed timeout
|
|
673
|
+
* @param {number} timeoutId - Timeout ID to clear
|
|
674
|
+
*/
|
|
675
|
+
clearManagedTimeout(timeoutId) {
|
|
676
|
+
if (timeoutId) {
|
|
677
|
+
clearTimeout(timeoutId);
|
|
678
|
+
this.timeouts.delete(timeoutId);
|
|
679
|
+
}
|
|
540
680
|
}
|
|
541
681
|
|
|
542
682
|
/**
|
|
@@ -557,8 +697,9 @@ export class Player extends EventEmitter {
|
|
|
557
697
|
}
|
|
558
698
|
|
|
559
699
|
// Clear existing text tracks
|
|
560
|
-
const existingTracks = this.
|
|
700
|
+
const existingTracks = this.trackElements;
|
|
561
701
|
existingTracks.forEach(track => track.remove());
|
|
702
|
+
this.invalidateTrackCache();
|
|
562
703
|
|
|
563
704
|
// Update media element
|
|
564
705
|
this.element.src = config.src;
|
|
@@ -586,6 +727,7 @@ export class Player extends EventEmitter {
|
|
|
586
727
|
|
|
587
728
|
this.element.appendChild(track);
|
|
588
729
|
});
|
|
730
|
+
this.invalidateTrackCache();
|
|
589
731
|
}
|
|
590
732
|
|
|
591
733
|
// Check if we need to change renderer type
|
|
@@ -895,7 +1037,7 @@ export class Player extends EventEmitter {
|
|
|
895
1037
|
// Audio Description
|
|
896
1038
|
async enableAudioDescription() {
|
|
897
1039
|
// Check if we have source elements with data-desc-src (even if audioDescriptionSrc is not set)
|
|
898
|
-
const hasSourceElementsWithDesc =
|
|
1040
|
+
const hasSourceElementsWithDesc = this.sourceElements.some(el => el.getAttribute('data-desc-src'));
|
|
899
1041
|
const hasTracksWithDesc = this.audioDescriptionCaptionTracks.length > 0;
|
|
900
1042
|
|
|
901
1043
|
if (!this.audioDescriptionSrc && !hasSourceElementsWithDesc && !hasTracksWithDesc) {
|
|
@@ -916,7 +1058,7 @@ export class Player extends EventEmitter {
|
|
|
916
1058
|
const currentSrc = this.element.currentSrc || this.element.src;
|
|
917
1059
|
|
|
918
1060
|
// Find the source element that matches the currently active source
|
|
919
|
-
const sourceElements =
|
|
1061
|
+
const sourceElements = this.sourceElements;
|
|
920
1062
|
let sourceElementToUpdate = null;
|
|
921
1063
|
let descSrc = this.audioDescriptionSrc;
|
|
922
1064
|
|
|
@@ -1060,11 +1202,12 @@ export class Player extends EventEmitter {
|
|
|
1060
1202
|
// After all new tracks are added, force browser to reload media element again
|
|
1061
1203
|
// This ensures new track elements are processed and new TextTrack objects are created
|
|
1062
1204
|
this.element.load();
|
|
1205
|
+
this.invalidateTrackCache();
|
|
1063
1206
|
|
|
1064
1207
|
// Wait for loadedmetadata event before accessing new TextTrack objects
|
|
1065
1208
|
const setupNewTracks = () => {
|
|
1066
1209
|
// Wait a bit more for browser to fully process the new track elements
|
|
1067
|
-
|
|
1210
|
+
this.setManagedTimeout(() => {
|
|
1068
1211
|
swappedTracksForTranscript.forEach((trackInfo) => {
|
|
1069
1212
|
const trackElement = trackInfo.trackElement;
|
|
1070
1213
|
const newTextTrack = trackElement.track;
|
|
@@ -1119,7 +1262,7 @@ export class Player extends EventEmitter {
|
|
|
1119
1262
|
// Update all source elements that have data-desc-src to their described versions
|
|
1120
1263
|
// Force browser to pick up changes by removing and re-adding source elements
|
|
1121
1264
|
// Get source elements (may have been defined in if block above, but get fresh list here)
|
|
1122
|
-
const allSourceElements =
|
|
1265
|
+
const allSourceElements = this.sourceElements;
|
|
1123
1266
|
const sourcesToUpdate = [];
|
|
1124
1267
|
|
|
1125
1268
|
allSourceElements.forEach((sourceEl) => {
|
|
@@ -1374,7 +1517,7 @@ export class Player extends EventEmitter {
|
|
|
1374
1517
|
}
|
|
1375
1518
|
|
|
1376
1519
|
// Check if we have source elements with data-desc-src (fallback method)
|
|
1377
|
-
const fallbackSourceElements =
|
|
1520
|
+
const fallbackSourceElements = this.sourceElements;
|
|
1378
1521
|
const hasSourceElementsWithDesc = fallbackSourceElements.some(el => el.getAttribute('data-desc-src'));
|
|
1379
1522
|
|
|
1380
1523
|
if (hasSourceElementsWithDesc) {
|
|
@@ -1433,6 +1576,7 @@ export class Player extends EventEmitter {
|
|
|
1433
1576
|
|
|
1434
1577
|
// Force reload
|
|
1435
1578
|
this.element.load();
|
|
1579
|
+
this.invalidateTrackCache();
|
|
1436
1580
|
} else {
|
|
1437
1581
|
// Fallback to updating element src directly (for videos without source elements)
|
|
1438
1582
|
this.element.src = this.audioDescriptionSrc;
|
|
@@ -1456,7 +1600,7 @@ export class Player extends EventEmitter {
|
|
|
1456
1600
|
// Seek to a tiny fraction to trigger poster hiding without actually moving
|
|
1457
1601
|
this.element.currentTime = 0.001;
|
|
1458
1602
|
// Then seek back to 0 after a brief moment to ensure poster stays hidden
|
|
1459
|
-
|
|
1603
|
+
this.setManagedTimeout(() => {
|
|
1460
1604
|
this.element.currentTime = 0;
|
|
1461
1605
|
}, 10);
|
|
1462
1606
|
}
|
|
@@ -1521,7 +1665,9 @@ export class Player extends EventEmitter {
|
|
|
1521
1665
|
const onMetadataLoaded = () => {
|
|
1522
1666
|
// Get fresh track references from the video element's textTracks collection
|
|
1523
1667
|
// This ensures we get the actual textTrack objects that the browser created
|
|
1524
|
-
|
|
1668
|
+
// Invalidate cache first to get fresh tracks after swap
|
|
1669
|
+
this.invalidateTrackCache();
|
|
1670
|
+
const allTextTracks = this.textTracks;
|
|
1525
1671
|
|
|
1526
1672
|
// Find the tracks that match our swapped tracks by language and kind
|
|
1527
1673
|
// Match by checking the track element's src attribute
|
|
@@ -1541,9 +1687,7 @@ export class Player extends EventEmitter {
|
|
|
1541
1687
|
if (track.language === srclang &&
|
|
1542
1688
|
(track.kind === kind || (kind === 'captions' && track.kind === 'subtitles'))) {
|
|
1543
1689
|
// Verify the src matches
|
|
1544
|
-
const trackElementForTrack =
|
|
1545
|
-
el => el.track === track
|
|
1546
|
-
);
|
|
1690
|
+
const trackElementForTrack = this.findTrackElement(track);
|
|
1547
1691
|
if (trackElementForTrack) {
|
|
1548
1692
|
const actualSrc = trackElementForTrack.getAttribute('src');
|
|
1549
1693
|
if (actualSrc === expectedSrc) {
|
|
@@ -1557,9 +1701,7 @@ export class Player extends EventEmitter {
|
|
|
1557
1701
|
|
|
1558
1702
|
// Verify the track element's src matches what we expect
|
|
1559
1703
|
if (foundTrack) {
|
|
1560
|
-
const trackElement =
|
|
1561
|
-
el => el.track === foundTrack
|
|
1562
|
-
);
|
|
1704
|
+
const trackElement = this.findTrackElement(foundTrack);
|
|
1563
1705
|
if (trackElement && trackElement.getAttribute('src') !== expectedSrc) {
|
|
1564
1706
|
return null;
|
|
1565
1707
|
}
|
|
@@ -1570,7 +1712,7 @@ export class Player extends EventEmitter {
|
|
|
1570
1712
|
|
|
1571
1713
|
if (freshTracks.length === 0) {
|
|
1572
1714
|
// Fallback: just reload after delay - transcript manager will find tracks itself
|
|
1573
|
-
|
|
1715
|
+
this.setManagedTimeout(() => {
|
|
1574
1716
|
if (this.transcriptManager && this.transcriptManager.loadTranscriptData) {
|
|
1575
1717
|
this.transcriptManager.loadTranscriptData();
|
|
1576
1718
|
}
|
|
@@ -1591,7 +1733,7 @@ export class Player extends EventEmitter {
|
|
|
1591
1733
|
if (loadedCount >= freshTracks.length) {
|
|
1592
1734
|
// Give a bit more time for cues to be fully parsed
|
|
1593
1735
|
// Also ensure we're getting the latest TextTrack references
|
|
1594
|
-
|
|
1736
|
+
this.setManagedTimeout(() => {
|
|
1595
1737
|
if (this.transcriptManager && this.transcriptManager.loadTranscriptData) {
|
|
1596
1738
|
// Force transcript manager to get fresh track references
|
|
1597
1739
|
// Clear any cached track references by forcing a fresh read
|
|
@@ -1599,12 +1741,11 @@ export class Player extends EventEmitter {
|
|
|
1599
1741
|
// which should now have the new TextTrack objects with the described captions
|
|
1600
1742
|
|
|
1601
1743
|
// Verify the tracks have the correct src before reloading transcript
|
|
1602
|
-
|
|
1744
|
+
this.invalidateTrackCache();
|
|
1745
|
+
const allTextTracks = this.textTracks;
|
|
1603
1746
|
const swappedTrackSrcs = swappedTracks.map(t => t.describedSrc);
|
|
1604
1747
|
const hasCorrectTracks = freshTracks.some(track => {
|
|
1605
|
-
const trackEl =
|
|
1606
|
-
el => el.track === track
|
|
1607
|
-
);
|
|
1748
|
+
const trackEl = this.findTrackElement(track);
|
|
1608
1749
|
return trackEl && swappedTrackSrcs.includes(trackEl.getAttribute('src'));
|
|
1609
1750
|
});
|
|
1610
1751
|
|
|
@@ -1624,9 +1765,7 @@ export class Player extends EventEmitter {
|
|
|
1624
1765
|
|
|
1625
1766
|
// Check if track has cues loaded
|
|
1626
1767
|
// Verify the track element's src matches the expected described src
|
|
1627
|
-
const trackElementForTrack =
|
|
1628
|
-
el => el.track === track
|
|
1629
|
-
);
|
|
1768
|
+
const trackElementForTrack = this.findTrackElement(track);
|
|
1630
1769
|
const actualSrc = trackElementForTrack ? trackElementForTrack.getAttribute('src') : null;
|
|
1631
1770
|
|
|
1632
1771
|
// Find the expected src from swappedTracks
|
|
@@ -1657,13 +1796,13 @@ export class Player extends EventEmitter {
|
|
|
1657
1796
|
// Wait for track to load
|
|
1658
1797
|
const onTrackLoad = () => {
|
|
1659
1798
|
// Wait a bit for cues to be fully parsed
|
|
1660
|
-
|
|
1799
|
+
this.setManagedTimeout(checkLoaded, 300);
|
|
1661
1800
|
};
|
|
1662
1801
|
|
|
1663
1802
|
if (track.readyState >= 2) {
|
|
1664
1803
|
// Already loaded, but might not have cues yet
|
|
1665
1804
|
// Wait a bit and check again
|
|
1666
|
-
|
|
1805
|
+
this.setManagedTimeout(() => {
|
|
1667
1806
|
if (track.cues && track.cues.length > 0) {
|
|
1668
1807
|
checkLoaded();
|
|
1669
1808
|
} else {
|
|
@@ -1685,16 +1824,16 @@ export class Player extends EventEmitter {
|
|
|
1685
1824
|
// Wait for loadedmetadata event which fires when browser processes track elements
|
|
1686
1825
|
// Also wait for the tracks to be fully processed after the second load()
|
|
1687
1826
|
const waitForTracks = () => {
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1827
|
+
// Wait a bit more to ensure new TextTrack objects are created
|
|
1828
|
+
this.setManagedTimeout(() => {
|
|
1829
|
+
if (this.element.readyState >= 1) { // HAVE_METADATA
|
|
1830
|
+
onMetadataLoaded();
|
|
1831
|
+
} else {
|
|
1832
|
+
this.element.addEventListener('loadedmetadata', onMetadataLoaded, { once: true });
|
|
1833
|
+
// Fallback timeout
|
|
1834
|
+
this.setManagedTimeout(onMetadataLoaded, 2000);
|
|
1835
|
+
}
|
|
1836
|
+
}, 500); // Wait 500ms after second load() for tracks to be processed
|
|
1698
1837
|
};
|
|
1699
1838
|
|
|
1700
1839
|
waitForTracks();
|
|
@@ -1739,7 +1878,7 @@ export class Player extends EventEmitter {
|
|
|
1739
1878
|
|
|
1740
1879
|
// Swap source elements back to original versions
|
|
1741
1880
|
// Check if we have source elements with data-orig-src
|
|
1742
|
-
const allSourceElements =
|
|
1881
|
+
const allSourceElements = this.sourceElements;
|
|
1743
1882
|
const hasSourceElementsToSwap = allSourceElements.some(el => el.getAttribute('data-orig-src'));
|
|
1744
1883
|
|
|
1745
1884
|
if (hasSourceElementsToSwap) {
|
|
@@ -1817,16 +1956,16 @@ export class Player extends EventEmitter {
|
|
|
1817
1956
|
this.play();
|
|
1818
1957
|
}
|
|
1819
1958
|
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1959
|
+
// Reload transcript if visible (after video metadata loaded, tracks should be available)
|
|
1960
|
+
// Reload regardless of whether caption tracks were swapped, in case tracks changed
|
|
1961
|
+
if (this.transcriptManager && this.transcriptManager.isVisible) {
|
|
1962
|
+
// Wait for tracks to load after source swap
|
|
1963
|
+
this.setManagedTimeout(() => {
|
|
1964
|
+
if (this.transcriptManager && this.transcriptManager.loadTranscriptData) {
|
|
1965
|
+
this.transcriptManager.loadTranscriptData();
|
|
1966
|
+
}
|
|
1967
|
+
}, 500);
|
|
1968
|
+
}
|
|
1830
1969
|
|
|
1831
1970
|
this.state.audioDescriptionEnabled = false;
|
|
1832
1971
|
this.emit('audiodescriptiondisabled');
|
|
@@ -1834,12 +1973,11 @@ export class Player extends EventEmitter {
|
|
|
1834
1973
|
|
|
1835
1974
|
async toggleAudioDescription() {
|
|
1836
1975
|
// Check if we have description tracks or audio-described video
|
|
1837
|
-
const
|
|
1838
|
-
const descriptionTrack = textTracks.find(track => track.kind === 'descriptions');
|
|
1976
|
+
const descriptionTrack = this.findTextTrack('descriptions');
|
|
1839
1977
|
|
|
1840
1978
|
// Check if we have audio-described video source (either from options or source elements with data-desc-src)
|
|
1841
1979
|
const hasAudioDescriptionSrc = this.audioDescriptionSrc ||
|
|
1842
|
-
|
|
1980
|
+
this.sourceElements.some(el => el.getAttribute('data-desc-src'));
|
|
1843
1981
|
|
|
1844
1982
|
if (descriptionTrack && hasAudioDescriptionSrc) {
|
|
1845
1983
|
// We have both: toggle description track AND swap caption tracks/sources
|
|
@@ -1850,7 +1988,35 @@ export class Player extends EventEmitter {
|
|
|
1850
1988
|
} else {
|
|
1851
1989
|
// Enable: swap caption tracks/sources and toggle description track on
|
|
1852
1990
|
await this.enableAudioDescription();
|
|
1853
|
-
|
|
1991
|
+
// Wait for tracks to be ready after source swap, then enable description track
|
|
1992
|
+
// Use a longer timeout to ensure tracks are loaded after source swap
|
|
1993
|
+
const enableDescriptionTrack = () => {
|
|
1994
|
+
this.invalidateTrackCache();
|
|
1995
|
+
const descTrack = this.findTextTrack('descriptions');
|
|
1996
|
+
if (descTrack) {
|
|
1997
|
+
// Set to 'hidden' first if it's in 'disabled' mode, then to 'showing'
|
|
1998
|
+
if (descTrack.mode === 'disabled') {
|
|
1999
|
+
descTrack.mode = 'hidden';
|
|
2000
|
+
// Use setTimeout to ensure the browser processes the mode change
|
|
2001
|
+
this.setManagedTimeout(() => {
|
|
2002
|
+
descTrack.mode = 'showing';
|
|
2003
|
+
}, 50);
|
|
2004
|
+
} else {
|
|
2005
|
+
descTrack.mode = 'showing';
|
|
2006
|
+
}
|
|
2007
|
+
} else if (this.element.readyState < 2) {
|
|
2008
|
+
// Tracks not ready yet, wait a bit more
|
|
2009
|
+
this.setManagedTimeout(enableDescriptionTrack, 100);
|
|
2010
|
+
}
|
|
2011
|
+
};
|
|
2012
|
+
// Wait for metadata to load first
|
|
2013
|
+
if (this.element.readyState >= 1) {
|
|
2014
|
+
this.setManagedTimeout(enableDescriptionTrack, 200);
|
|
2015
|
+
} else {
|
|
2016
|
+
this.element.addEventListener('loadedmetadata', () => {
|
|
2017
|
+
this.setManagedTimeout(enableDescriptionTrack, 200);
|
|
2018
|
+
}, { once: true });
|
|
2019
|
+
}
|
|
1854
2020
|
}
|
|
1855
2021
|
} else if (descriptionTrack) {
|
|
1856
2022
|
// Only description track, no audio-described video source to swap
|
|
@@ -2397,9 +2563,28 @@ export class Player extends EventEmitter {
|
|
|
2397
2563
|
}
|
|
2398
2564
|
|
|
2399
2565
|
// Logging
|
|
2400
|
-
log(
|
|
2401
|
-
if (this.options.debug) {
|
|
2402
|
-
|
|
2566
|
+
log(...messages) {
|
|
2567
|
+
if (!this.options.debug) {
|
|
2568
|
+
return;
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2571
|
+
let type = 'log';
|
|
2572
|
+
if (messages.length > 0) {
|
|
2573
|
+
const potentialType = messages[messages.length - 1];
|
|
2574
|
+
if (typeof potentialType === 'string' && console[potentialType]) {
|
|
2575
|
+
type = potentialType;
|
|
2576
|
+
messages = messages.slice(0, -1);
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
if (messages.length === 0) {
|
|
2581
|
+
messages = [''];
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
if (typeof console[type] === 'function') {
|
|
2585
|
+
console[type]('[VidPly]', ...messages);
|
|
2586
|
+
} else {
|
|
2587
|
+
console.log('[VidPly]', ...messages);
|
|
2403
2588
|
}
|
|
2404
2589
|
}
|
|
2405
2590
|
|
|
@@ -2493,7 +2678,7 @@ export class Player extends EventEmitter {
|
|
|
2493
2678
|
if (this.signLanguageWrapper && this.signLanguageWrapper.style.display !== 'none') {
|
|
2494
2679
|
// Use setTimeout to ensure layout has updated after fullscreen transition
|
|
2495
2680
|
// Longer delay to account for CSS transition animations and layout recalculation
|
|
2496
|
-
|
|
2681
|
+
this.setManagedTimeout(() => {
|
|
2497
2682
|
// Use requestAnimationFrame to ensure the browser has fully rendered the layout
|
|
2498
2683
|
requestAnimationFrame(() => {
|
|
2499
2684
|
// Clear saved size and reset to default for the new container size
|
|
@@ -2580,6 +2765,29 @@ export class Player extends EventEmitter {
|
|
|
2580
2765
|
this.fullscreenChangeHandler = null;
|
|
2581
2766
|
}
|
|
2582
2767
|
|
|
2768
|
+
// Cleanup all managed timeouts
|
|
2769
|
+
this.timeouts.forEach(timeoutId => clearTimeout(timeoutId));
|
|
2770
|
+
this.timeouts.clear();
|
|
2771
|
+
|
|
2772
|
+
// Cleanup metadata handling
|
|
2773
|
+
if (this.metadataCueChangeHandler) {
|
|
2774
|
+
const textTracks = this.textTracks;
|
|
2775
|
+
const metadataTrack = textTracks.find(track => track.kind === 'metadata');
|
|
2776
|
+
if (metadataTrack) {
|
|
2777
|
+
metadataTrack.removeEventListener('cuechange', this.metadataCueChangeHandler);
|
|
2778
|
+
}
|
|
2779
|
+
this.metadataCueChangeHandler = null;
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2782
|
+
if (this.metadataAlertHandlers && this.metadataAlertHandlers.size > 0) {
|
|
2783
|
+
this.metadataAlertHandlers.forEach(({ button, handler }) => {
|
|
2784
|
+
if (button && handler) {
|
|
2785
|
+
button.removeEventListener('click', handler);
|
|
2786
|
+
}
|
|
2787
|
+
});
|
|
2788
|
+
this.metadataAlertHandlers.clear();
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2583
2791
|
// Remove container
|
|
2584
2792
|
if (this.container && this.container.parentNode) {
|
|
2585
2793
|
this.container.parentNode.insertBefore(this.element, this.container);
|
|
@@ -2588,6 +2796,424 @@ export class Player extends EventEmitter {
|
|
|
2588
2796
|
|
|
2589
2797
|
this.removeAllListeners();
|
|
2590
2798
|
}
|
|
2799
|
+
|
|
2800
|
+
/**
|
|
2801
|
+
* Setup metadata track handling
|
|
2802
|
+
* This enables metadata tracks and listens for cue changes to trigger actions
|
|
2803
|
+
*/
|
|
2804
|
+
setupMetadataHandling() {
|
|
2805
|
+
const setupMetadata = () => {
|
|
2806
|
+
const textTracks = this.textTracks;
|
|
2807
|
+
const metadataTrack = textTracks.find(track => track.kind === 'metadata');
|
|
2808
|
+
|
|
2809
|
+
if (metadataTrack) {
|
|
2810
|
+
// Enable the metadata track so cuechange events fire
|
|
2811
|
+
// Use 'hidden' mode so it doesn't display anything, but events still work
|
|
2812
|
+
if (metadataTrack.mode === 'disabled') {
|
|
2813
|
+
metadataTrack.mode = 'hidden';
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
// Remove existing listener if any
|
|
2817
|
+
if (this.metadataCueChangeHandler) {
|
|
2818
|
+
metadataTrack.removeEventListener('cuechange', this.metadataCueChangeHandler);
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
// Add event listener for cue changes
|
|
2822
|
+
this.metadataCueChangeHandler = () => {
|
|
2823
|
+
const activeCues = Array.from(metadataTrack.activeCues || []);
|
|
2824
|
+
if (activeCues.length > 0) {
|
|
2825
|
+
// Debug logging
|
|
2826
|
+
if (this.options.debug) {
|
|
2827
|
+
this.log('[Metadata] Active cues:', activeCues.map(c => ({
|
|
2828
|
+
start: c.startTime,
|
|
2829
|
+
end: c.endTime,
|
|
2830
|
+
text: c.text
|
|
2831
|
+
})));
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
activeCues.forEach(cue => {
|
|
2835
|
+
this.handleMetadataCue(cue);
|
|
2836
|
+
});
|
|
2837
|
+
};
|
|
2838
|
+
|
|
2839
|
+
metadataTrack.addEventListener('cuechange', this.metadataCueChangeHandler);
|
|
2840
|
+
|
|
2841
|
+
// Debug: Log metadata track setup
|
|
2842
|
+
if (this.options.debug) {
|
|
2843
|
+
const cueCount = metadataTrack.cues ? metadataTrack.cues.length : 0;
|
|
2844
|
+
this.log('[Metadata] Track enabled,', cueCount, 'cues available');
|
|
2845
|
+
}
|
|
2846
|
+
} else if (this.options.debug) {
|
|
2847
|
+
this.log('[Metadata] No metadata track found');
|
|
2848
|
+
}
|
|
2849
|
+
};
|
|
2850
|
+
|
|
2851
|
+
// Try immediately
|
|
2852
|
+
setupMetadata();
|
|
2853
|
+
|
|
2854
|
+
// Also try after loadedmetadata event (tracks might not be ready yet)
|
|
2855
|
+
this.on('loadedmetadata', setupMetadata);
|
|
2856
|
+
}
|
|
2857
|
+
|
|
2858
|
+
normalizeMetadataSelector(selector) {
|
|
2859
|
+
if (!selector) {
|
|
2860
|
+
return null;
|
|
2861
|
+
}
|
|
2862
|
+
const trimmed = selector.trim();
|
|
2863
|
+
if (!trimmed) {
|
|
2864
|
+
return null;
|
|
2865
|
+
}
|
|
2866
|
+
if (trimmed.startsWith('#') || trimmed.startsWith('.') || trimmed.startsWith('[')) {
|
|
2867
|
+
return trimmed;
|
|
2868
|
+
}
|
|
2869
|
+
return `#${trimmed}`;
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2872
|
+
resolveMetadataConfig(map, key) {
|
|
2873
|
+
if (!map || !key) {
|
|
2874
|
+
return null;
|
|
2875
|
+
}
|
|
2876
|
+
if (Object.prototype.hasOwnProperty.call(map, key)) {
|
|
2877
|
+
return map[key];
|
|
2878
|
+
}
|
|
2879
|
+
const withoutHash = key.replace(/^#/, '');
|
|
2880
|
+
if (Object.prototype.hasOwnProperty.call(map, withoutHash)) {
|
|
2881
|
+
return map[withoutHash];
|
|
2882
|
+
}
|
|
2883
|
+
return null;
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2886
|
+
cacheMetadataAlertContent(element, config = {}) {
|
|
2887
|
+
if (!element) {
|
|
2888
|
+
return;
|
|
2889
|
+
}
|
|
2890
|
+
const titleSelector = config.titleSelector || '[data-vidply-alert-title], h3, header';
|
|
2891
|
+
const messageSelector = config.messageSelector || '[data-vidply-alert-message], p';
|
|
2892
|
+
|
|
2893
|
+
const titleEl = element.querySelector(titleSelector);
|
|
2894
|
+
if (titleEl && !titleEl.dataset.vidplyAlertTitleOriginal) {
|
|
2895
|
+
titleEl.dataset.vidplyAlertTitleOriginal = titleEl.textContent.trim();
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2898
|
+
const messageEl = element.querySelector(messageSelector);
|
|
2899
|
+
if (messageEl && !messageEl.dataset.vidplyAlertMessageOriginal) {
|
|
2900
|
+
messageEl.dataset.vidplyAlertMessageOriginal = messageEl.textContent.trim();
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2904
|
+
restoreMetadataAlertContent(element, config = {}) {
|
|
2905
|
+
if (!element) {
|
|
2906
|
+
return;
|
|
2907
|
+
}
|
|
2908
|
+
const titleSelector = config.titleSelector || '[data-vidply-alert-title], h3, header';
|
|
2909
|
+
const messageSelector = config.messageSelector || '[data-vidply-alert-message], p';
|
|
2910
|
+
|
|
2911
|
+
const titleEl = element.querySelector(titleSelector);
|
|
2912
|
+
if (titleEl && titleEl.dataset.vidplyAlertTitleOriginal) {
|
|
2913
|
+
titleEl.textContent = titleEl.dataset.vidplyAlertTitleOriginal;
|
|
2914
|
+
}
|
|
2915
|
+
|
|
2916
|
+
const messageEl = element.querySelector(messageSelector);
|
|
2917
|
+
if (messageEl && messageEl.dataset.vidplyAlertMessageOriginal) {
|
|
2918
|
+
messageEl.textContent = messageEl.dataset.vidplyAlertMessageOriginal;
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
|
|
2922
|
+
focusMetadataTarget(target, fallbackElement = null) {
|
|
2923
|
+
if (!target || target === 'none') {
|
|
2924
|
+
return;
|
|
2925
|
+
}
|
|
2926
|
+
|
|
2927
|
+
if (target === 'alert' && fallbackElement) {
|
|
2928
|
+
fallbackElement.focus();
|
|
2929
|
+
return;
|
|
2930
|
+
}
|
|
2931
|
+
|
|
2932
|
+
if (target === 'player') {
|
|
2933
|
+
if (this.container) {
|
|
2934
|
+
this.container.focus();
|
|
2935
|
+
}
|
|
2936
|
+
return;
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2939
|
+
if (target === 'media') {
|
|
2940
|
+
this.element.focus();
|
|
2941
|
+
return;
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2944
|
+
if (target === 'playButton') {
|
|
2945
|
+
const playButton = this.controlBar?.controls?.playPause;
|
|
2946
|
+
if (playButton) {
|
|
2947
|
+
playButton.focus();
|
|
2948
|
+
}
|
|
2949
|
+
return;
|
|
2950
|
+
}
|
|
2951
|
+
|
|
2952
|
+
if (typeof target === 'string') {
|
|
2953
|
+
const targetElement = document.querySelector(target);
|
|
2954
|
+
if (targetElement) {
|
|
2955
|
+
if (targetElement.tabIndex === -1 && !targetElement.hasAttribute('tabindex')) {
|
|
2956
|
+
targetElement.setAttribute('tabindex', '-1');
|
|
2957
|
+
}
|
|
2958
|
+
targetElement.focus();
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
handleMetadataAlert(selector, options = {}) {
|
|
2964
|
+
if (!selector) {
|
|
2965
|
+
return;
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
const config = this.resolveMetadataConfig(this.options.metadataAlerts, selector) || {};
|
|
2969
|
+
const element = options.element || document.querySelector(selector);
|
|
2970
|
+
|
|
2971
|
+
if (!element) {
|
|
2972
|
+
if (this.options.debug) {
|
|
2973
|
+
this.log('[Metadata] Alert element not found:', selector);
|
|
2974
|
+
}
|
|
2975
|
+
return;
|
|
2976
|
+
}
|
|
2977
|
+
|
|
2978
|
+
if (this.options.debug) {
|
|
2979
|
+
this.log('[Metadata] Handling alert', selector, { reason: options.reason, config });
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
this.cacheMetadataAlertContent(element, config);
|
|
2983
|
+
|
|
2984
|
+
if (!element.dataset.vidplyAlertOriginalDisplay) {
|
|
2985
|
+
element.dataset.vidplyAlertOriginalDisplay = element.style.display || '';
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2988
|
+
if (!element.dataset.vidplyAlertDisplay) {
|
|
2989
|
+
element.dataset.vidplyAlertDisplay = config.display || 'block';
|
|
2990
|
+
}
|
|
2991
|
+
|
|
2992
|
+
const shouldShow = options.show !== undefined ? options.show : (config.show !== false);
|
|
2993
|
+
if (shouldShow) {
|
|
2994
|
+
const displayValue = config.display || element.dataset.vidplyAlertDisplay || 'block';
|
|
2995
|
+
element.style.display = displayValue;
|
|
2996
|
+
element.hidden = false;
|
|
2997
|
+
element.removeAttribute('hidden');
|
|
2998
|
+
element.setAttribute('aria-hidden', 'false');
|
|
2999
|
+
element.setAttribute('data-vidply-alert-active', 'true');
|
|
3000
|
+
}
|
|
3001
|
+
|
|
3002
|
+
const shouldReset = config.resetContent !== false && options.reason === 'focus';
|
|
3003
|
+
if (shouldReset) {
|
|
3004
|
+
this.restoreMetadataAlertContent(element, config);
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
const shouldFocus = options.focus !== undefined
|
|
3008
|
+
? options.focus
|
|
3009
|
+
: (config.focusOnShow ?? (options.reason !== 'focus'));
|
|
3010
|
+
|
|
3011
|
+
if (shouldShow && shouldFocus) {
|
|
3012
|
+
if (element.tabIndex === -1 && !element.hasAttribute('tabindex')) {
|
|
3013
|
+
element.setAttribute('tabindex', '-1');
|
|
3014
|
+
}
|
|
3015
|
+
element.focus();
|
|
3016
|
+
}
|
|
3017
|
+
|
|
3018
|
+
if (shouldShow && config.autoScroll !== false && options.autoScroll !== false) {
|
|
3019
|
+
element.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
3020
|
+
}
|
|
3021
|
+
|
|
3022
|
+
const continueSelector = config.continueButton;
|
|
3023
|
+
if (continueSelector) {
|
|
3024
|
+
let continueButton = null;
|
|
3025
|
+
if (continueSelector === 'self') {
|
|
3026
|
+
continueButton = element;
|
|
3027
|
+
} else if (element.matches(continueSelector)) {
|
|
3028
|
+
continueButton = element;
|
|
3029
|
+
} else {
|
|
3030
|
+
continueButton = element.querySelector(continueSelector) || document.querySelector(continueSelector);
|
|
3031
|
+
}
|
|
3032
|
+
|
|
3033
|
+
if (continueButton && !this.metadataAlertHandlers.has(selector)) {
|
|
3034
|
+
const handler = () => {
|
|
3035
|
+
const hideOnContinue = config.hideOnContinue !== false;
|
|
3036
|
+
if (hideOnContinue) {
|
|
3037
|
+
const originalDisplay = element.dataset.vidplyAlertOriginalDisplay || '';
|
|
3038
|
+
element.style.display = config.hideDisplay || originalDisplay || 'none';
|
|
3039
|
+
element.setAttribute('aria-hidden', 'true');
|
|
3040
|
+
element.removeAttribute('data-vidply-alert-active');
|
|
3041
|
+
}
|
|
3042
|
+
|
|
3043
|
+
if (config.resume !== false && this.state.paused) {
|
|
3044
|
+
this.play();
|
|
3045
|
+
}
|
|
3046
|
+
|
|
3047
|
+
const focusTarget = config.focusTarget || 'playButton';
|
|
3048
|
+
this.setManagedTimeout(() => {
|
|
3049
|
+
this.focusMetadataTarget(focusTarget, element);
|
|
3050
|
+
}, config.focusDelay ?? 100);
|
|
3051
|
+
};
|
|
3052
|
+
|
|
3053
|
+
continueButton.addEventListener('click', handler);
|
|
3054
|
+
this.metadataAlertHandlers.set(selector, { button: continueButton, handler });
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
|
|
3058
|
+
return element;
|
|
3059
|
+
}
|
|
3060
|
+
|
|
3061
|
+
handleMetadataHashtags(hashtags) {
|
|
3062
|
+
if (!Array.isArray(hashtags) || hashtags.length === 0) {
|
|
3063
|
+
return;
|
|
3064
|
+
}
|
|
3065
|
+
|
|
3066
|
+
const configMap = this.options.metadataHashtags;
|
|
3067
|
+
if (!configMap) {
|
|
3068
|
+
return;
|
|
3069
|
+
}
|
|
3070
|
+
|
|
3071
|
+
hashtags.forEach(tag => {
|
|
3072
|
+
const config = this.resolveMetadataConfig(configMap, tag);
|
|
3073
|
+
if (!config) {
|
|
3074
|
+
return;
|
|
3075
|
+
}
|
|
3076
|
+
|
|
3077
|
+
const selector = this.normalizeMetadataSelector(config.alert || config.selector || config.target);
|
|
3078
|
+
if (!selector) {
|
|
3079
|
+
return;
|
|
3080
|
+
}
|
|
3081
|
+
|
|
3082
|
+
const element = document.querySelector(selector);
|
|
3083
|
+
if (!element) {
|
|
3084
|
+
if (this.options.debug) {
|
|
3085
|
+
this.log('[Metadata] Hashtag target not found:', selector);
|
|
3086
|
+
}
|
|
3087
|
+
return;
|
|
3088
|
+
}
|
|
3089
|
+
|
|
3090
|
+
if (this.options.debug) {
|
|
3091
|
+
this.log('[Metadata] Handling hashtag', tag, { selector, config });
|
|
3092
|
+
}
|
|
3093
|
+
|
|
3094
|
+
this.cacheMetadataAlertContent(element, config);
|
|
3095
|
+
|
|
3096
|
+
if (config.title) {
|
|
3097
|
+
const titleSelector = config.titleSelector || '[data-vidply-alert-title], h3, header';
|
|
3098
|
+
const titleEl = element.querySelector(titleSelector);
|
|
3099
|
+
if (titleEl) {
|
|
3100
|
+
titleEl.textContent = config.title;
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
|
|
3104
|
+
if (config.message) {
|
|
3105
|
+
const messageSelector = config.messageSelector || '[data-vidply-alert-message], p';
|
|
3106
|
+
const messageEl = element.querySelector(messageSelector);
|
|
3107
|
+
if (messageEl) {
|
|
3108
|
+
messageEl.textContent = config.message;
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
|
|
3112
|
+
const show = config.show !== false;
|
|
3113
|
+
const focus = config.focus !== undefined ? config.focus : false;
|
|
3114
|
+
|
|
3115
|
+
this.handleMetadataAlert(selector, {
|
|
3116
|
+
element,
|
|
3117
|
+
show,
|
|
3118
|
+
focus,
|
|
3119
|
+
autoScroll: config.autoScroll,
|
|
3120
|
+
reason: 'hashtag'
|
|
3121
|
+
});
|
|
3122
|
+
});
|
|
3123
|
+
}
|
|
3124
|
+
|
|
3125
|
+
/**
|
|
3126
|
+
* Handle individual metadata cues
|
|
3127
|
+
* Parses metadata text and emits events or triggers actions
|
|
3128
|
+
*/
|
|
3129
|
+
handleMetadataCue(cue) {
|
|
3130
|
+
const text = cue.text.trim();
|
|
3131
|
+
|
|
3132
|
+
// Debug logging
|
|
3133
|
+
if (this.options.debug) {
|
|
3134
|
+
this.log('[Metadata] Processing cue:', {
|
|
3135
|
+
time: cue.startTime,
|
|
3136
|
+
text: text
|
|
3137
|
+
});
|
|
3138
|
+
}
|
|
3139
|
+
|
|
3140
|
+
// Emit a generic metadata event that developers can listen to
|
|
3141
|
+
this.emit('metadata', {
|
|
3142
|
+
time: cue.startTime,
|
|
3143
|
+
endTime: cue.endTime,
|
|
3144
|
+
text: text,
|
|
3145
|
+
cue: cue
|
|
3146
|
+
});
|
|
3147
|
+
|
|
3148
|
+
// Parse for specific commands (examples based on wwa_meta.vtt format)
|
|
3149
|
+
if (text.includes('PAUSE')) {
|
|
3150
|
+
// Automatically pause the video
|
|
3151
|
+
if (!this.state.paused) {
|
|
3152
|
+
if (this.options.debug) {
|
|
3153
|
+
this.log('[Metadata] Pausing video at', cue.startTime);
|
|
3154
|
+
}
|
|
3155
|
+
this.pause();
|
|
3156
|
+
}
|
|
3157
|
+
// Also emit event for developers who want to listen
|
|
3158
|
+
this.emit('metadata:pause', { time: cue.startTime, text: text });
|
|
3159
|
+
}
|
|
3160
|
+
|
|
3161
|
+
// Parse for focus directives
|
|
3162
|
+
const focusMatch = text.match(/FOCUS:([\w#-]+)/);
|
|
3163
|
+
if (focusMatch) {
|
|
3164
|
+
const targetSelector = focusMatch[1];
|
|
3165
|
+
const normalizedSelector = this.normalizeMetadataSelector(targetSelector);
|
|
3166
|
+
// Automatically focus the target element
|
|
3167
|
+
const targetElement = normalizedSelector ? document.querySelector(normalizedSelector) : null;
|
|
3168
|
+
if (targetElement) {
|
|
3169
|
+
if (this.options.debug) {
|
|
3170
|
+
this.log('[Metadata] Focusing element:', normalizedSelector);
|
|
3171
|
+
}
|
|
3172
|
+
// Make element focusable if it isn't already
|
|
3173
|
+
if (targetElement.tabIndex === -1 && !targetElement.hasAttribute('tabindex')) {
|
|
3174
|
+
targetElement.setAttribute('tabindex', '-1');
|
|
3175
|
+
}
|
|
3176
|
+
// Use setTimeout to ensure DOM is ready
|
|
3177
|
+
this.setManagedTimeout(() => {
|
|
3178
|
+
targetElement.focus();
|
|
3179
|
+
// Scroll element into view if needed
|
|
3180
|
+
targetElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
3181
|
+
}, 10);
|
|
3182
|
+
} else if (this.options.debug) {
|
|
3183
|
+
this.log('[Metadata] Element not found:', normalizedSelector || targetSelector);
|
|
3184
|
+
}
|
|
3185
|
+
// Also emit event for developers who want to listen
|
|
3186
|
+
this.emit('metadata:focus', {
|
|
3187
|
+
time: cue.startTime,
|
|
3188
|
+
target: targetSelector,
|
|
3189
|
+
selector: normalizedSelector,
|
|
3190
|
+
element: targetElement,
|
|
3191
|
+
text: text
|
|
3192
|
+
});
|
|
3193
|
+
|
|
3194
|
+
if (normalizedSelector) {
|
|
3195
|
+
this.handleMetadataAlert(normalizedSelector, {
|
|
3196
|
+
element: targetElement,
|
|
3197
|
+
reason: 'focus'
|
|
3198
|
+
});
|
|
3199
|
+
}
|
|
3200
|
+
}
|
|
3201
|
+
|
|
3202
|
+
// Parse for hashtag references
|
|
3203
|
+
const hashtags = text.match(/#[\w-]+/g);
|
|
3204
|
+
if (hashtags) {
|
|
3205
|
+
if (this.options.debug) {
|
|
3206
|
+
this.log('[Metadata] Hashtags found:', hashtags);
|
|
3207
|
+
}
|
|
3208
|
+
this.emit('metadata:hashtags', {
|
|
3209
|
+
time: cue.startTime,
|
|
3210
|
+
hashtags: hashtags,
|
|
3211
|
+
text: text
|
|
3212
|
+
});
|
|
3213
|
+
|
|
3214
|
+
this.handleMetadataHashtags(hashtags);
|
|
3215
|
+
}
|
|
3216
|
+
}
|
|
2591
3217
|
}
|
|
2592
3218
|
|
|
2593
3219
|
// Static instances tracker for pause others functionality
|