vidply 1.0.8 → 1.0.10
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 +154 -52
- package/dist/vidply.esm.js +1867 -731
- package/dist/vidply.esm.js.map +3 -3
- package/dist/vidply.esm.min.js +3 -3
- package/dist/vidply.esm.min.meta.json +28 -10
- package/dist/vidply.js +1867 -731
- package/dist/vidply.js.map +3 -3
- package/dist/vidply.min.css +1 -1
- package/dist/vidply.min.js +3 -3
- package/dist/vidply.min.meta.json +27 -9
- package/package.json +1 -1
- package/src/controls/ControlBar.js +24 -14
- package/src/controls/TranscriptManager.js +658 -591
- package/src/core/Player.js +831 -331
- package/src/i18n/translations.js +55 -5
- package/src/icons/Icons.js +2 -2
- package/src/styles/vidply.css +154 -52
- package/src/utils/DraggableResizable.js +771 -0
package/src/core/Player.js
CHANGED
|
@@ -16,6 +16,7 @@ import {HLSRenderer} from '../renderers/HLSRenderer.js';
|
|
|
16
16
|
import {createPlayOverlay} from '../icons/Icons.js';
|
|
17
17
|
import {i18n} from '../i18n/i18n.js';
|
|
18
18
|
import {StorageManager} from '../utils/StorageManager.js';
|
|
19
|
+
import {DraggableResizable} from '../utils/DraggableResizable.js';
|
|
19
20
|
|
|
20
21
|
export class Player extends EventEmitter {
|
|
21
22
|
constructor(element, options = {}) {
|
|
@@ -143,6 +144,8 @@ export class Player extends EventEmitter {
|
|
|
143
144
|
screenReaderAnnouncements: true,
|
|
144
145
|
highContrast: false,
|
|
145
146
|
focusHighlight: true,
|
|
147
|
+
metadataAlerts: {},
|
|
148
|
+
metadataHashtags: {},
|
|
146
149
|
|
|
147
150
|
// Languages
|
|
148
151
|
language: 'en',
|
|
@@ -166,6 +169,9 @@ export class Player extends EventEmitter {
|
|
|
166
169
|
...options
|
|
167
170
|
};
|
|
168
171
|
|
|
172
|
+
this.options.metadataAlerts = this.options.metadataAlerts || {};
|
|
173
|
+
this.options.metadataHashtags = this.options.metadataHashtags || {};
|
|
174
|
+
|
|
169
175
|
// Storage manager
|
|
170
176
|
this.storage = new StorageManager('vidply');
|
|
171
177
|
|
|
@@ -209,6 +215,18 @@ export class Player extends EventEmitter {
|
|
|
209
215
|
this.originalAudioDescriptionSource = null;
|
|
210
216
|
// Store caption tracks that should be swapped for audio description
|
|
211
217
|
this.audioDescriptionCaptionTracks = [];
|
|
218
|
+
this._audioDescriptionDesiredState = false;
|
|
219
|
+
|
|
220
|
+
// DOM query cache (for performance optimization)
|
|
221
|
+
this._textTracksCache = null;
|
|
222
|
+
this._textTracksDirty = true;
|
|
223
|
+
this._sourceElementsCache = null;
|
|
224
|
+
this._sourceElementsDirty = true;
|
|
225
|
+
this._trackElementsCache = null;
|
|
226
|
+
this._trackElementsDirty = true;
|
|
227
|
+
|
|
228
|
+
// Timeout management (for cleanup)
|
|
229
|
+
this.timeouts = new Set();
|
|
212
230
|
|
|
213
231
|
// Components
|
|
214
232
|
this.container = null;
|
|
@@ -217,6 +235,10 @@ export class Player extends EventEmitter {
|
|
|
217
235
|
this.captionManager = null;
|
|
218
236
|
this.keyboardManager = null;
|
|
219
237
|
this.settingsDialog = null;
|
|
238
|
+
|
|
239
|
+
// Metadata handling
|
|
240
|
+
this.metadataCueChangeHandler = null;
|
|
241
|
+
this.metadataAlertHandlers = new Map();
|
|
220
242
|
|
|
221
243
|
// Initialize
|
|
222
244
|
this.init();
|
|
@@ -264,6 +286,9 @@ export class Player extends EventEmitter {
|
|
|
264
286
|
if (this.options.transcript || this.options.transcriptButton) {
|
|
265
287
|
this.transcriptManager = new TranscriptManager(this);
|
|
266
288
|
}
|
|
289
|
+
|
|
290
|
+
// Always set up metadata track handling (independent of transcript)
|
|
291
|
+
this.setupMetadataHandling();
|
|
267
292
|
|
|
268
293
|
// Initialize keyboard controls
|
|
269
294
|
if (this.options.keyboard) {
|
|
@@ -373,7 +398,6 @@ export class Player extends EventEmitter {
|
|
|
373
398
|
// This allows custom controls to work on iOS devices
|
|
374
399
|
if (this.element.tagName === 'VIDEO' && this.options.playsInline) {
|
|
375
400
|
this.element.setAttribute('playsinline', '');
|
|
376
|
-
this.element.setAttribute('webkit-playsinline', ''); // For older iOS versions
|
|
377
401
|
this.element.playsInline = true; // Property version
|
|
378
402
|
}
|
|
379
403
|
|
|
@@ -399,6 +423,12 @@ export class Player extends EventEmitter {
|
|
|
399
423
|
if (this.element.tagName === 'VIDEO') {
|
|
400
424
|
this.createPlayButtonOverlay();
|
|
401
425
|
}
|
|
426
|
+
|
|
427
|
+
// Store reference to player on element for easy access
|
|
428
|
+
this.element.vidply = this;
|
|
429
|
+
|
|
430
|
+
// Add to static instances array
|
|
431
|
+
Player.instances.push(this);
|
|
402
432
|
|
|
403
433
|
// Make video/audio element clickable to toggle play/pause
|
|
404
434
|
this.element.style.cursor = 'pointer';
|
|
@@ -408,6 +438,16 @@ export class Player extends EventEmitter {
|
|
|
408
438
|
this.toggle();
|
|
409
439
|
}
|
|
410
440
|
});
|
|
441
|
+
|
|
442
|
+
this.on('play', () => {
|
|
443
|
+
this.hidePosterOverlay();
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
this.on('timeupdate', () => {
|
|
447
|
+
if (this.state.currentTime > 0) {
|
|
448
|
+
this.hidePosterOverlay();
|
|
449
|
+
}
|
|
450
|
+
});
|
|
411
451
|
}
|
|
412
452
|
|
|
413
453
|
createPlayButtonOverlay() {
|
|
@@ -447,7 +487,7 @@ export class Player extends EventEmitter {
|
|
|
447
487
|
}
|
|
448
488
|
|
|
449
489
|
// Check for source elements with audio description attributes
|
|
450
|
-
const sourceElements = this.
|
|
490
|
+
const sourceElements = this.sourceElements;
|
|
451
491
|
for (const sourceEl of sourceElements) {
|
|
452
492
|
const descSrc = sourceEl.getAttribute('data-desc-src');
|
|
453
493
|
const origSrc = sourceEl.getAttribute('data-orig-src');
|
|
@@ -490,7 +530,7 @@ export class Player extends EventEmitter {
|
|
|
490
530
|
// Check for caption/subtitle tracks with audio description versions
|
|
491
531
|
// Only tracks with explicit data-desc-src attribute are swapped (no auto-detection to avoid 404 errors)
|
|
492
532
|
// Description tracks (kind="descriptions") are NOT swapped - they're for transcripts
|
|
493
|
-
const trackElements = this.
|
|
533
|
+
const trackElements = this.trackElements;
|
|
494
534
|
trackElements.forEach(trackEl => {
|
|
495
535
|
const trackKind = trackEl.getAttribute('kind');
|
|
496
536
|
const trackDescSrc = trackEl.getAttribute('data-desc-src');
|
|
@@ -537,6 +577,144 @@ export class Player extends EventEmitter {
|
|
|
537
577
|
this.log(`Using ${renderer.name} renderer`);
|
|
538
578
|
this.renderer = new renderer(this);
|
|
539
579
|
await this.renderer.init();
|
|
580
|
+
|
|
581
|
+
// Invalidate cache after renderer initialization (tracks may have changed)
|
|
582
|
+
this.invalidateTrackCache();
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Get cached text tracks array
|
|
587
|
+
* @returns {Array} Array of text tracks
|
|
588
|
+
*/
|
|
589
|
+
get textTracks() {
|
|
590
|
+
if (!this._textTracksCache || this._textTracksDirty) {
|
|
591
|
+
this._textTracksCache = Array.from(this.element.textTracks || []);
|
|
592
|
+
this._textTracksDirty = false;
|
|
593
|
+
}
|
|
594
|
+
return this._textTracksCache;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Get cached source elements array
|
|
599
|
+
* @returns {Array} Array of source elements
|
|
600
|
+
*/
|
|
601
|
+
get sourceElements() {
|
|
602
|
+
if (!this._sourceElementsCache || this._sourceElementsDirty) {
|
|
603
|
+
this._sourceElementsCache = Array.from(this.element.querySelectorAll('source'));
|
|
604
|
+
this._sourceElementsDirty = false;
|
|
605
|
+
}
|
|
606
|
+
return this._sourceElementsCache;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Get cached track elements array
|
|
611
|
+
* @returns {Array} Array of track elements
|
|
612
|
+
*/
|
|
613
|
+
get trackElements() {
|
|
614
|
+
if (!this._trackElementsCache || this._trackElementsDirty) {
|
|
615
|
+
this._trackElementsCache = Array.from(this.element.querySelectorAll('track'));
|
|
616
|
+
this._trackElementsDirty = false;
|
|
617
|
+
}
|
|
618
|
+
return this._trackElementsCache;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Invalidate DOM query cache (call when tracks/sources change)
|
|
623
|
+
*/
|
|
624
|
+
invalidateTrackCache() {
|
|
625
|
+
this._textTracksDirty = true;
|
|
626
|
+
this._trackElementsDirty = true;
|
|
627
|
+
this._sourceElementsDirty = true;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Find a text track by kind and optionally language
|
|
632
|
+
* @param {string} kind - Track kind (captions, subtitles, descriptions, chapters, metadata)
|
|
633
|
+
* @param {string} [language] - Optional language code
|
|
634
|
+
* @returns {TextTrack|null} Found track or null
|
|
635
|
+
*/
|
|
636
|
+
findTextTrack(kind, language = null) {
|
|
637
|
+
const tracks = this.textTracks;
|
|
638
|
+
if (language) {
|
|
639
|
+
return tracks.find(t => t.kind === kind && t.language === language);
|
|
640
|
+
}
|
|
641
|
+
return tracks.find(t => t.kind === kind);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Find a source element by attribute
|
|
646
|
+
* @param {string} attribute - Attribute name (e.g., 'data-desc-src')
|
|
647
|
+
* @param {string} [value] - Optional attribute value
|
|
648
|
+
* @returns {Element|null} Found source element or null
|
|
649
|
+
*/
|
|
650
|
+
findSourceElement(attribute, value = null) {
|
|
651
|
+
const sources = this.sourceElements;
|
|
652
|
+
if (value) {
|
|
653
|
+
return sources.find(el => el.getAttribute(attribute) === value);
|
|
654
|
+
}
|
|
655
|
+
return sources.find(el => el.hasAttribute(attribute));
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Find a track element by its associated TextTrack
|
|
660
|
+
* @param {TextTrack} track - The TextTrack object
|
|
661
|
+
* @returns {Element|null} Found track element or null
|
|
662
|
+
*/
|
|
663
|
+
findTrackElement(track) {
|
|
664
|
+
return this.trackElements.find(el => el.track === track);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
showPosterOverlay() {
|
|
668
|
+
if (!this.videoWrapper || this.element.tagName !== 'VIDEO') {
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const poster =
|
|
673
|
+
this.element.getAttribute('poster') ||
|
|
674
|
+
this.element.poster ||
|
|
675
|
+
this.options.poster;
|
|
676
|
+
|
|
677
|
+
if (!poster) {
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
this.videoWrapper.style.setProperty('--vidply-poster-image', `url("${poster}")`);
|
|
682
|
+
this.videoWrapper.classList.add('vidply-forced-poster');
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
hidePosterOverlay() {
|
|
686
|
+
if (!this.videoWrapper) {
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
this.videoWrapper.classList.remove('vidply-forced-poster');
|
|
691
|
+
this.videoWrapper.style.removeProperty('--vidply-poster-image');
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Set a managed timeout that will be cleaned up on destroy
|
|
696
|
+
* @param {Function} callback - Callback function
|
|
697
|
+
* @param {number} delay - Delay in milliseconds
|
|
698
|
+
* @returns {number} Timeout ID
|
|
699
|
+
*/
|
|
700
|
+
setManagedTimeout(callback, delay) {
|
|
701
|
+
const timeoutId = setTimeout(() => {
|
|
702
|
+
this.timeouts.delete(timeoutId);
|
|
703
|
+
callback();
|
|
704
|
+
}, delay);
|
|
705
|
+
this.timeouts.add(timeoutId);
|
|
706
|
+
return timeoutId;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Clear a managed timeout
|
|
711
|
+
* @param {number} timeoutId - Timeout ID to clear
|
|
712
|
+
*/
|
|
713
|
+
clearManagedTimeout(timeoutId) {
|
|
714
|
+
if (timeoutId) {
|
|
715
|
+
clearTimeout(timeoutId);
|
|
716
|
+
this.timeouts.delete(timeoutId);
|
|
717
|
+
}
|
|
540
718
|
}
|
|
541
719
|
|
|
542
720
|
/**
|
|
@@ -557,8 +735,9 @@ export class Player extends EventEmitter {
|
|
|
557
735
|
}
|
|
558
736
|
|
|
559
737
|
// Clear existing text tracks
|
|
560
|
-
const existingTracks = this.
|
|
738
|
+
const existingTracks = this.trackElements;
|
|
561
739
|
existingTracks.forEach(track => track.remove());
|
|
740
|
+
this.invalidateTrackCache();
|
|
562
741
|
|
|
563
742
|
// Update media element
|
|
564
743
|
this.element.src = config.src;
|
|
@@ -586,6 +765,7 @@ export class Player extends EventEmitter {
|
|
|
586
765
|
|
|
587
766
|
this.element.appendChild(track);
|
|
588
767
|
});
|
|
768
|
+
this.invalidateTrackCache();
|
|
589
769
|
}
|
|
590
770
|
|
|
591
771
|
// Check if we need to change renderer type
|
|
@@ -895,7 +1075,7 @@ export class Player extends EventEmitter {
|
|
|
895
1075
|
// Audio Description
|
|
896
1076
|
async enableAudioDescription() {
|
|
897
1077
|
// Check if we have source elements with data-desc-src (even if audioDescriptionSrc is not set)
|
|
898
|
-
const hasSourceElementsWithDesc =
|
|
1078
|
+
const hasSourceElementsWithDesc = this.sourceElements.some(el => el.getAttribute('data-desc-src'));
|
|
899
1079
|
const hasTracksWithDesc = this.audioDescriptionCaptionTracks.length > 0;
|
|
900
1080
|
|
|
901
1081
|
if (!this.audioDescriptionSrc && !hasSourceElementsWithDesc && !hasTracksWithDesc) {
|
|
@@ -906,6 +1086,11 @@ export class Player extends EventEmitter {
|
|
|
906
1086
|
// Store current playback state
|
|
907
1087
|
const currentTime = this.state.currentTime;
|
|
908
1088
|
const wasPlaying = this.state.playing;
|
|
1089
|
+
const shouldKeepPoster = !wasPlaying && currentTime === 0;
|
|
1090
|
+
|
|
1091
|
+
if (shouldKeepPoster) {
|
|
1092
|
+
this.showPosterOverlay();
|
|
1093
|
+
}
|
|
909
1094
|
|
|
910
1095
|
// Store swapped tracks for transcript reload (declare at function scope)
|
|
911
1096
|
let swappedTracksForTranscript = [];
|
|
@@ -916,7 +1101,7 @@ export class Player extends EventEmitter {
|
|
|
916
1101
|
const currentSrc = this.element.currentSrc || this.element.src;
|
|
917
1102
|
|
|
918
1103
|
// Find the source element that matches the currently active source
|
|
919
|
-
const sourceElements =
|
|
1104
|
+
const sourceElements = this.sourceElements;
|
|
920
1105
|
let sourceElementToUpdate = null;
|
|
921
1106
|
let descSrc = this.audioDescriptionSrc;
|
|
922
1107
|
|
|
@@ -1060,11 +1245,12 @@ export class Player extends EventEmitter {
|
|
|
1060
1245
|
// After all new tracks are added, force browser to reload media element again
|
|
1061
1246
|
// This ensures new track elements are processed and new TextTrack objects are created
|
|
1062
1247
|
this.element.load();
|
|
1248
|
+
this.invalidateTrackCache();
|
|
1063
1249
|
|
|
1064
1250
|
// Wait for loadedmetadata event before accessing new TextTrack objects
|
|
1065
1251
|
const setupNewTracks = () => {
|
|
1066
1252
|
// Wait a bit more for browser to fully process the new track elements
|
|
1067
|
-
|
|
1253
|
+
this.setManagedTimeout(() => {
|
|
1068
1254
|
swappedTracksForTranscript.forEach((trackInfo) => {
|
|
1069
1255
|
const trackElement = trackInfo.trackElement;
|
|
1070
1256
|
const newTextTrack = trackElement.track;
|
|
@@ -1119,7 +1305,7 @@ export class Player extends EventEmitter {
|
|
|
1119
1305
|
// Update all source elements that have data-desc-src to their described versions
|
|
1120
1306
|
// Force browser to pick up changes by removing and re-adding source elements
|
|
1121
1307
|
// Get source elements (may have been defined in if block above, but get fresh list here)
|
|
1122
|
-
const allSourceElements =
|
|
1308
|
+
const allSourceElements = this.sourceElements;
|
|
1123
1309
|
const sourcesToUpdate = [];
|
|
1124
1310
|
|
|
1125
1311
|
allSourceElements.forEach((sourceEl) => {
|
|
@@ -1173,8 +1359,17 @@ export class Player extends EventEmitter {
|
|
|
1173
1359
|
if (sourceInfo.descSrc) {
|
|
1174
1360
|
newSource.setAttribute('data-desc-src', sourceInfo.descSrc);
|
|
1175
1361
|
}
|
|
1176
|
-
this.element.
|
|
1362
|
+
const firstTrack = this.element.querySelector('track');
|
|
1363
|
+
if (firstTrack) {
|
|
1364
|
+
this.element.insertBefore(newSource, firstTrack);
|
|
1365
|
+
} else {
|
|
1366
|
+
this.element.appendChild(newSource);
|
|
1367
|
+
}
|
|
1177
1368
|
});
|
|
1369
|
+
|
|
1370
|
+
// Ensure cached source references are refreshed after rebuilding the list
|
|
1371
|
+
this._sourceElementsDirty = true;
|
|
1372
|
+
this._sourceElementsCache = null;
|
|
1178
1373
|
|
|
1179
1374
|
// Force reload by calling load() on the element
|
|
1180
1375
|
// This should pick up the new src attributes from the re-added source elements
|
|
@@ -1193,28 +1388,23 @@ export class Player extends EventEmitter {
|
|
|
1193
1388
|
// Wait a bit more for tracks to be recognized and loaded after video metadata loads
|
|
1194
1389
|
await new Promise(resolve => setTimeout(resolve, 300));
|
|
1195
1390
|
|
|
1196
|
-
//
|
|
1197
|
-
if (
|
|
1198
|
-
|
|
1199
|
-
// Setting readyState check or seeking to 0.001 seconds will hide the poster
|
|
1200
|
-
if (this.element.readyState >= 1) { // HAVE_METADATA
|
|
1201
|
-
// Seek to a tiny fraction to trigger poster hiding without actually moving
|
|
1202
|
-
this.element.currentTime = 0.001;
|
|
1203
|
-
// Then seek back to 0 after a brief moment to ensure poster stays hidden
|
|
1204
|
-
setTimeout(() => {
|
|
1205
|
-
this.element.currentTime = 0;
|
|
1206
|
-
}, 10);
|
|
1207
|
-
}
|
|
1391
|
+
// Restore playback position (avoid forcing first frame if still at start)
|
|
1392
|
+
if (currentTime > 0) {
|
|
1393
|
+
this.seek(currentTime);
|
|
1208
1394
|
}
|
|
1209
1395
|
|
|
1210
|
-
// Restore playback position
|
|
1211
|
-
this.seek(currentTime);
|
|
1212
|
-
|
|
1213
1396
|
if (wasPlaying) {
|
|
1214
1397
|
this.play();
|
|
1215
1398
|
}
|
|
1216
1399
|
|
|
1400
|
+
if (!shouldKeepPoster) {
|
|
1401
|
+
this.hidePosterOverlay();
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1217
1404
|
// Update state and emit event
|
|
1405
|
+
if (!this._audioDescriptionDesiredState) {
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1218
1408
|
this.state.audioDescriptionEnabled = true;
|
|
1219
1409
|
this.emit('audiodescriptionenabled');
|
|
1220
1410
|
} else {
|
|
@@ -1374,7 +1564,7 @@ export class Player extends EventEmitter {
|
|
|
1374
1564
|
}
|
|
1375
1565
|
|
|
1376
1566
|
// Check if we have source elements with data-desc-src (fallback method)
|
|
1377
|
-
const fallbackSourceElements =
|
|
1567
|
+
const fallbackSourceElements = this.sourceElements;
|
|
1378
1568
|
const hasSourceElementsWithDesc = fallbackSourceElements.some(el => el.getAttribute('data-desc-src'));
|
|
1379
1569
|
|
|
1380
1570
|
if (hasSourceElementsWithDesc) {
|
|
@@ -1433,6 +1623,7 @@ export class Player extends EventEmitter {
|
|
|
1433
1623
|
|
|
1434
1624
|
// Force reload
|
|
1435
1625
|
this.element.load();
|
|
1626
|
+
this.invalidateTrackCache();
|
|
1436
1627
|
} else {
|
|
1437
1628
|
// Fallback to updating element src directly (for videos without source elements)
|
|
1438
1629
|
this.element.src = this.audioDescriptionSrc;
|
|
@@ -1456,14 +1647,16 @@ export class Player extends EventEmitter {
|
|
|
1456
1647
|
// Seek to a tiny fraction to trigger poster hiding without actually moving
|
|
1457
1648
|
this.element.currentTime = 0.001;
|
|
1458
1649
|
// Then seek back to 0 after a brief moment to ensure poster stays hidden
|
|
1459
|
-
|
|
1650
|
+
this.setManagedTimeout(() => {
|
|
1460
1651
|
this.element.currentTime = 0;
|
|
1461
1652
|
}, 10);
|
|
1462
1653
|
}
|
|
1463
1654
|
}
|
|
1464
1655
|
|
|
1465
|
-
// Restore playback position
|
|
1466
|
-
|
|
1656
|
+
// Restore playback position (avoid forcing first frame if still at start)
|
|
1657
|
+
if (currentTime > 0) {
|
|
1658
|
+
this.seek(currentTime);
|
|
1659
|
+
}
|
|
1467
1660
|
|
|
1468
1661
|
if (wasPlaying) {
|
|
1469
1662
|
this.play();
|
|
@@ -1521,7 +1714,9 @@ export class Player extends EventEmitter {
|
|
|
1521
1714
|
const onMetadataLoaded = () => {
|
|
1522
1715
|
// Get fresh track references from the video element's textTracks collection
|
|
1523
1716
|
// This ensures we get the actual textTrack objects that the browser created
|
|
1524
|
-
|
|
1717
|
+
// Invalidate cache first to get fresh tracks after swap
|
|
1718
|
+
this.invalidateTrackCache();
|
|
1719
|
+
const allTextTracks = this.textTracks;
|
|
1525
1720
|
|
|
1526
1721
|
// Find the tracks that match our swapped tracks by language and kind
|
|
1527
1722
|
// Match by checking the track element's src attribute
|
|
@@ -1541,9 +1736,7 @@ export class Player extends EventEmitter {
|
|
|
1541
1736
|
if (track.language === srclang &&
|
|
1542
1737
|
(track.kind === kind || (kind === 'captions' && track.kind === 'subtitles'))) {
|
|
1543
1738
|
// Verify the src matches
|
|
1544
|
-
const trackElementForTrack =
|
|
1545
|
-
el => el.track === track
|
|
1546
|
-
);
|
|
1739
|
+
const trackElementForTrack = this.findTrackElement(track);
|
|
1547
1740
|
if (trackElementForTrack) {
|
|
1548
1741
|
const actualSrc = trackElementForTrack.getAttribute('src');
|
|
1549
1742
|
if (actualSrc === expectedSrc) {
|
|
@@ -1557,9 +1750,7 @@ export class Player extends EventEmitter {
|
|
|
1557
1750
|
|
|
1558
1751
|
// Verify the track element's src matches what we expect
|
|
1559
1752
|
if (foundTrack) {
|
|
1560
|
-
const trackElement =
|
|
1561
|
-
el => el.track === foundTrack
|
|
1562
|
-
);
|
|
1753
|
+
const trackElement = this.findTrackElement(foundTrack);
|
|
1563
1754
|
if (trackElement && trackElement.getAttribute('src') !== expectedSrc) {
|
|
1564
1755
|
return null;
|
|
1565
1756
|
}
|
|
@@ -1570,7 +1761,7 @@ export class Player extends EventEmitter {
|
|
|
1570
1761
|
|
|
1571
1762
|
if (freshTracks.length === 0) {
|
|
1572
1763
|
// Fallback: just reload after delay - transcript manager will find tracks itself
|
|
1573
|
-
|
|
1764
|
+
this.setManagedTimeout(() => {
|
|
1574
1765
|
if (this.transcriptManager && this.transcriptManager.loadTranscriptData) {
|
|
1575
1766
|
this.transcriptManager.loadTranscriptData();
|
|
1576
1767
|
}
|
|
@@ -1591,7 +1782,7 @@ export class Player extends EventEmitter {
|
|
|
1591
1782
|
if (loadedCount >= freshTracks.length) {
|
|
1592
1783
|
// Give a bit more time for cues to be fully parsed
|
|
1593
1784
|
// Also ensure we're getting the latest TextTrack references
|
|
1594
|
-
|
|
1785
|
+
this.setManagedTimeout(() => {
|
|
1595
1786
|
if (this.transcriptManager && this.transcriptManager.loadTranscriptData) {
|
|
1596
1787
|
// Force transcript manager to get fresh track references
|
|
1597
1788
|
// Clear any cached track references by forcing a fresh read
|
|
@@ -1599,12 +1790,11 @@ export class Player extends EventEmitter {
|
|
|
1599
1790
|
// which should now have the new TextTrack objects with the described captions
|
|
1600
1791
|
|
|
1601
1792
|
// Verify the tracks have the correct src before reloading transcript
|
|
1602
|
-
|
|
1793
|
+
this.invalidateTrackCache();
|
|
1794
|
+
const allTextTracks = this.textTracks;
|
|
1603
1795
|
const swappedTrackSrcs = swappedTracks.map(t => t.describedSrc);
|
|
1604
1796
|
const hasCorrectTracks = freshTracks.some(track => {
|
|
1605
|
-
const trackEl =
|
|
1606
|
-
el => el.track === track
|
|
1607
|
-
);
|
|
1797
|
+
const trackEl = this.findTrackElement(track);
|
|
1608
1798
|
return trackEl && swappedTrackSrcs.includes(trackEl.getAttribute('src'));
|
|
1609
1799
|
});
|
|
1610
1800
|
|
|
@@ -1624,9 +1814,7 @@ export class Player extends EventEmitter {
|
|
|
1624
1814
|
|
|
1625
1815
|
// Check if track has cues loaded
|
|
1626
1816
|
// Verify the track element's src matches the expected described src
|
|
1627
|
-
const trackElementForTrack =
|
|
1628
|
-
el => el.track === track
|
|
1629
|
-
);
|
|
1817
|
+
const trackElementForTrack = this.findTrackElement(track);
|
|
1630
1818
|
const actualSrc = trackElementForTrack ? trackElementForTrack.getAttribute('src') : null;
|
|
1631
1819
|
|
|
1632
1820
|
// Find the expected src from swappedTracks
|
|
@@ -1657,13 +1845,13 @@ export class Player extends EventEmitter {
|
|
|
1657
1845
|
// Wait for track to load
|
|
1658
1846
|
const onTrackLoad = () => {
|
|
1659
1847
|
// Wait a bit for cues to be fully parsed
|
|
1660
|
-
|
|
1848
|
+
this.setManagedTimeout(checkLoaded, 300);
|
|
1661
1849
|
};
|
|
1662
1850
|
|
|
1663
1851
|
if (track.readyState >= 2) {
|
|
1664
1852
|
// Already loaded, but might not have cues yet
|
|
1665
1853
|
// Wait a bit and check again
|
|
1666
|
-
|
|
1854
|
+
this.setManagedTimeout(() => {
|
|
1667
1855
|
if (track.cues && track.cues.length > 0) {
|
|
1668
1856
|
checkLoaded();
|
|
1669
1857
|
} else {
|
|
@@ -1685,16 +1873,16 @@ export class Player extends EventEmitter {
|
|
|
1685
1873
|
// Wait for loadedmetadata event which fires when browser processes track elements
|
|
1686
1874
|
// Also wait for the tracks to be fully processed after the second load()
|
|
1687
1875
|
const waitForTracks = () => {
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1876
|
+
// Wait a bit more to ensure new TextTrack objects are created
|
|
1877
|
+
this.setManagedTimeout(() => {
|
|
1878
|
+
if (this.element.readyState >= 1) { // HAVE_METADATA
|
|
1879
|
+
onMetadataLoaded();
|
|
1880
|
+
} else {
|
|
1881
|
+
this.element.addEventListener('loadedmetadata', onMetadataLoaded, { once: true });
|
|
1882
|
+
// Fallback timeout
|
|
1883
|
+
this.setManagedTimeout(onMetadataLoaded, 2000);
|
|
1884
|
+
}
|
|
1885
|
+
}, 500); // Wait 500ms after second load() for tracks to be processed
|
|
1698
1886
|
};
|
|
1699
1887
|
|
|
1700
1888
|
waitForTracks();
|
|
@@ -1715,6 +1903,14 @@ export class Player extends EventEmitter {
|
|
|
1715
1903
|
}
|
|
1716
1904
|
}
|
|
1717
1905
|
|
|
1906
|
+
if (!shouldKeepPoster) {
|
|
1907
|
+
this.hidePosterOverlay();
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
if (!this._audioDescriptionDesiredState) {
|
|
1911
|
+
return;
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1718
1914
|
this.state.audioDescriptionEnabled = true;
|
|
1719
1915
|
this.emit('audiodescriptionenabled');
|
|
1720
1916
|
}
|
|
@@ -1739,7 +1935,7 @@ export class Player extends EventEmitter {
|
|
|
1739
1935
|
|
|
1740
1936
|
// Swap source elements back to original versions
|
|
1741
1937
|
// Check if we have source elements with data-orig-src
|
|
1742
|
-
const allSourceElements =
|
|
1938
|
+
const allSourceElements = this.sourceElements;
|
|
1743
1939
|
const hasSourceElementsToSwap = allSourceElements.some(el => el.getAttribute('data-orig-src'));
|
|
1744
1940
|
|
|
1745
1941
|
if (hasSourceElementsToSwap) {
|
|
@@ -1789,8 +1985,17 @@ export class Player extends EventEmitter {
|
|
|
1789
1985
|
if (sourceInfo.descSrc) {
|
|
1790
1986
|
newSource.setAttribute('data-desc-src', sourceInfo.descSrc);
|
|
1791
1987
|
}
|
|
1792
|
-
this.element.
|
|
1988
|
+
const firstTrack = this.element.querySelector('track');
|
|
1989
|
+
if (firstTrack) {
|
|
1990
|
+
this.element.insertBefore(newSource, firstTrack);
|
|
1991
|
+
} else {
|
|
1992
|
+
this.element.appendChild(newSource);
|
|
1993
|
+
}
|
|
1793
1994
|
});
|
|
1995
|
+
|
|
1996
|
+
// Ensure cached source references are refreshed after rebuilding the list
|
|
1997
|
+
this._sourceElementsDirty = true;
|
|
1998
|
+
this._sourceElementsCache = null;
|
|
1794
1999
|
|
|
1795
2000
|
// Force reload
|
|
1796
2001
|
this.element.load();
|
|
@@ -1801,31 +2006,44 @@ export class Player extends EventEmitter {
|
|
|
1801
2006
|
this.element.load();
|
|
1802
2007
|
}
|
|
1803
2008
|
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
2009
|
+
if (currentTime > 0 || wasPlaying) {
|
|
2010
|
+
// Wait for new source to load
|
|
2011
|
+
await new Promise((resolve) => {
|
|
2012
|
+
const onLoadedMetadata = () => {
|
|
2013
|
+
this.element.removeEventListener('loadedmetadata', onLoadedMetadata);
|
|
2014
|
+
resolve();
|
|
2015
|
+
};
|
|
2016
|
+
this.element.addEventListener('loadedmetadata', onLoadedMetadata);
|
|
2017
|
+
});
|
|
1812
2018
|
|
|
1813
|
-
|
|
1814
|
-
|
|
2019
|
+
if (currentTime > 0) {
|
|
2020
|
+
this.seek(currentTime);
|
|
2021
|
+
}
|
|
1815
2022
|
|
|
1816
|
-
|
|
1817
|
-
|
|
2023
|
+
if (wasPlaying) {
|
|
2024
|
+
this.play();
|
|
2025
|
+
}
|
|
1818
2026
|
}
|
|
1819
2027
|
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
2028
|
+
if (!wasPlaying && currentTime === 0) {
|
|
2029
|
+
this.showPosterOverlay();
|
|
2030
|
+
} else {
|
|
2031
|
+
this.hidePosterOverlay();
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
// Reload transcript if visible (after video metadata loaded, tracks should be available)
|
|
2035
|
+
// Reload regardless of whether caption tracks were swapped, in case tracks changed
|
|
2036
|
+
if (this.transcriptManager && this.transcriptManager.isVisible) {
|
|
2037
|
+
// Wait for tracks to load after source swap
|
|
2038
|
+
this.setManagedTimeout(() => {
|
|
2039
|
+
if (this.transcriptManager && this.transcriptManager.loadTranscriptData) {
|
|
2040
|
+
this.transcriptManager.loadTranscriptData();
|
|
2041
|
+
}
|
|
2042
|
+
}, 500);
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
if (this._audioDescriptionDesiredState) {
|
|
2046
|
+
return;
|
|
1829
2047
|
}
|
|
1830
2048
|
|
|
1831
2049
|
this.state.audioDescriptionEnabled = false;
|
|
@@ -1834,32 +2052,63 @@ export class Player extends EventEmitter {
|
|
|
1834
2052
|
|
|
1835
2053
|
async toggleAudioDescription() {
|
|
1836
2054
|
// Check if we have description tracks or audio-described video
|
|
1837
|
-
const
|
|
1838
|
-
const descriptionTrack = textTracks.find(track => track.kind === 'descriptions');
|
|
2055
|
+
const descriptionTrack = this.findTextTrack('descriptions');
|
|
1839
2056
|
|
|
1840
2057
|
// Check if we have audio-described video source (either from options or source elements with data-desc-src)
|
|
1841
2058
|
const hasAudioDescriptionSrc = this.audioDescriptionSrc ||
|
|
1842
|
-
|
|
2059
|
+
this.sourceElements.some(el => el.getAttribute('data-desc-src'));
|
|
1843
2060
|
|
|
1844
2061
|
if (descriptionTrack && hasAudioDescriptionSrc) {
|
|
1845
2062
|
// We have both: toggle description track AND swap caption tracks/sources
|
|
1846
2063
|
if (this.state.audioDescriptionEnabled) {
|
|
2064
|
+
this._audioDescriptionDesiredState = false;
|
|
1847
2065
|
// Disable: toggle description track off and swap captions/sources back
|
|
1848
2066
|
descriptionTrack.mode = 'hidden';
|
|
1849
2067
|
await this.disableAudioDescription();
|
|
1850
2068
|
} else {
|
|
2069
|
+
this._audioDescriptionDesiredState = true;
|
|
1851
2070
|
// Enable: swap caption tracks/sources and toggle description track on
|
|
1852
2071
|
await this.enableAudioDescription();
|
|
1853
|
-
|
|
2072
|
+
// Wait for tracks to be ready after source swap, then enable description track
|
|
2073
|
+
// Use a longer timeout to ensure tracks are loaded after source swap
|
|
2074
|
+
const enableDescriptionTrack = () => {
|
|
2075
|
+
this.invalidateTrackCache();
|
|
2076
|
+
const descTrack = this.findTextTrack('descriptions');
|
|
2077
|
+
if (descTrack) {
|
|
2078
|
+
// Set to 'hidden' first if it's in 'disabled' mode, then to 'showing'
|
|
2079
|
+
if (descTrack.mode === 'disabled') {
|
|
2080
|
+
descTrack.mode = 'hidden';
|
|
2081
|
+
// Use setTimeout to ensure the browser processes the mode change
|
|
2082
|
+
this.setManagedTimeout(() => {
|
|
2083
|
+
descTrack.mode = 'showing';
|
|
2084
|
+
}, 50);
|
|
2085
|
+
} else {
|
|
2086
|
+
descTrack.mode = 'showing';
|
|
2087
|
+
}
|
|
2088
|
+
} else if (this.element.readyState < 2) {
|
|
2089
|
+
// Tracks not ready yet, wait a bit more
|
|
2090
|
+
this.setManagedTimeout(enableDescriptionTrack, 100);
|
|
2091
|
+
}
|
|
2092
|
+
};
|
|
2093
|
+
// Wait for metadata to load first
|
|
2094
|
+
if (this.element.readyState >= 1) {
|
|
2095
|
+
this.setManagedTimeout(enableDescriptionTrack, 200);
|
|
2096
|
+
} else {
|
|
2097
|
+
this.element.addEventListener('loadedmetadata', () => {
|
|
2098
|
+
this.setManagedTimeout(enableDescriptionTrack, 200);
|
|
2099
|
+
}, { once: true });
|
|
2100
|
+
}
|
|
1854
2101
|
}
|
|
1855
2102
|
} else if (descriptionTrack) {
|
|
1856
2103
|
// Only description track, no audio-described video source to swap
|
|
1857
2104
|
// Toggle description track
|
|
1858
2105
|
if (descriptionTrack.mode === 'showing') {
|
|
2106
|
+
this._audioDescriptionDesiredState = false;
|
|
1859
2107
|
descriptionTrack.mode = 'hidden';
|
|
1860
2108
|
this.state.audioDescriptionEnabled = false;
|
|
1861
2109
|
this.emit('audiodescriptiondisabled');
|
|
1862
2110
|
} else {
|
|
2111
|
+
this._audioDescriptionDesiredState = true;
|
|
1863
2112
|
descriptionTrack.mode = 'showing';
|
|
1864
2113
|
this.state.audioDescriptionEnabled = true;
|
|
1865
2114
|
this.emit('audiodescriptionenabled');
|
|
@@ -1867,8 +2116,10 @@ export class Player extends EventEmitter {
|
|
|
1867
2116
|
} else if (hasAudioDescriptionSrc) {
|
|
1868
2117
|
// Use audio-described video source (no description track)
|
|
1869
2118
|
if (this.state.audioDescriptionEnabled) {
|
|
2119
|
+
this._audioDescriptionDesiredState = false;
|
|
1870
2120
|
await this.disableAudioDescription();
|
|
1871
2121
|
} else {
|
|
2122
|
+
this._audioDescriptionDesiredState = true;
|
|
1872
2123
|
await this.enableAudioDescription();
|
|
1873
2124
|
}
|
|
1874
2125
|
}
|
|
@@ -1998,235 +2249,27 @@ export class Player extends EventEmitter {
|
|
|
1998
2249
|
setupSignLanguageInteraction() {
|
|
1999
2250
|
if (!this.signLanguageWrapper) return;
|
|
2000
2251
|
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
startX = e.clientX;
|
|
2019
|
-
startY = e.clientY;
|
|
2020
|
-
const rect = this.signLanguageWrapper.getBoundingClientRect();
|
|
2021
|
-
startLeft = rect.left;
|
|
2022
|
-
startTop = rect.top;
|
|
2023
|
-
this.signLanguageWrapper.classList.add('vidply-sign-dragging');
|
|
2024
|
-
};
|
|
2025
|
-
|
|
2026
|
-
// Mouse resize on handles
|
|
2027
|
-
const onMouseDownHandle = (e) => {
|
|
2028
|
-
if (!e.target.classList.contains('vidply-sign-resize-handle')) return;
|
|
2029
|
-
e.preventDefault();
|
|
2030
|
-
e.stopPropagation();
|
|
2031
|
-
isResizing = true;
|
|
2032
|
-
resizeDirection = e.target.getAttribute('data-direction');
|
|
2033
|
-
startX = e.clientX;
|
|
2034
|
-
startY = e.clientY;
|
|
2035
|
-
const rect = this.signLanguageWrapper.getBoundingClientRect();
|
|
2036
|
-
startLeft = rect.left;
|
|
2037
|
-
startTop = rect.top;
|
|
2038
|
-
startWidth = rect.width;
|
|
2039
|
-
startHeight = rect.height;
|
|
2040
|
-
this.signLanguageWrapper.classList.add('vidply-sign-resizing');
|
|
2041
|
-
};
|
|
2042
|
-
|
|
2043
|
-
const onMouseMove = (e) => {
|
|
2044
|
-
if (isDragging) {
|
|
2045
|
-
const deltaX = e.clientX - startX;
|
|
2046
|
-
const deltaY = e.clientY - startY;
|
|
2047
|
-
|
|
2048
|
-
// Get videoWrapper and container dimensions
|
|
2049
|
-
const videoWrapperRect = this.videoWrapper.getBoundingClientRect();
|
|
2050
|
-
const containerRect = this.container.getBoundingClientRect();
|
|
2051
|
-
const wrapperRect = this.signLanguageWrapper.getBoundingClientRect();
|
|
2052
|
-
|
|
2053
|
-
// Calculate videoWrapper position relative to container
|
|
2054
|
-
const videoWrapperLeft = videoWrapperRect.left - containerRect.left;
|
|
2055
|
-
const videoWrapperTop = videoWrapperRect.top - containerRect.top;
|
|
2056
|
-
|
|
2057
|
-
// Calculate new position (in client coordinates)
|
|
2058
|
-
let newLeft = startLeft + deltaX - containerRect.left;
|
|
2059
|
-
let newTop = startTop + deltaY - containerRect.top;
|
|
2060
|
-
|
|
2061
|
-
const controlsHeight = 95; // Height of controls when visible
|
|
2062
|
-
|
|
2063
|
-
// Constrain to videoWrapper bounds (ensuring it stays above controls)
|
|
2064
|
-
newLeft = Math.max(videoWrapperLeft, Math.min(newLeft, videoWrapperLeft + videoWrapperRect.width - wrapperRect.width));
|
|
2065
|
-
newTop = Math.max(videoWrapperTop, Math.min(newTop, videoWrapperTop + videoWrapperRect.height - wrapperRect.height - controlsHeight));
|
|
2066
|
-
|
|
2067
|
-
this.signLanguageWrapper.style.left = `${newLeft}px`;
|
|
2068
|
-
this.signLanguageWrapper.style.top = `${newTop}px`;
|
|
2069
|
-
this.signLanguageWrapper.style.right = 'auto';
|
|
2070
|
-
this.signLanguageWrapper.style.bottom = 'auto';
|
|
2071
|
-
// Remove position classes
|
|
2072
|
-
this.signLanguageWrapper.classList.remove(...Array.from(this.signLanguageWrapper.classList).filter(c => c.startsWith('vidply-sign-position-')));
|
|
2073
|
-
} else if (isResizing) {
|
|
2074
|
-
const deltaX = e.clientX - startX;
|
|
2075
|
-
|
|
2076
|
-
// Get videoWrapper and container dimensions
|
|
2077
|
-
const videoWrapperRect = this.videoWrapper.getBoundingClientRect();
|
|
2078
|
-
const containerRect = this.container.getBoundingClientRect();
|
|
2079
|
-
|
|
2080
|
-
let newWidth = startWidth;
|
|
2081
|
-
let newLeft = startLeft - containerRect.left;
|
|
2082
|
-
|
|
2083
|
-
// Only resize width, let height auto-adjust to maintain aspect ratio
|
|
2084
|
-
if (resizeDirection.includes('e')) {
|
|
2085
|
-
newWidth = Math.max(150, startWidth + deltaX);
|
|
2086
|
-
// Constrain width to not exceed videoWrapper right edge
|
|
2087
|
-
const maxWidth = (videoWrapperRect.right - startLeft);
|
|
2088
|
-
newWidth = Math.min(newWidth, maxWidth);
|
|
2089
|
-
}
|
|
2090
|
-
if (resizeDirection.includes('w')) {
|
|
2091
|
-
const proposedWidth = Math.max(150, startWidth - deltaX);
|
|
2092
|
-
const proposedLeft = startLeft + (startWidth - proposedWidth) - containerRect.left;
|
|
2093
|
-
// Constrain to not go beyond videoWrapper left edge
|
|
2094
|
-
const videoWrapperLeft = videoWrapperRect.left - containerRect.left;
|
|
2095
|
-
if (proposedLeft >= videoWrapperLeft) {
|
|
2096
|
-
newWidth = proposedWidth;
|
|
2097
|
-
newLeft = proposedLeft;
|
|
2098
|
-
}
|
|
2099
|
-
}
|
|
2100
|
-
|
|
2101
|
-
this.signLanguageWrapper.style.width = `${newWidth}px`;
|
|
2102
|
-
this.signLanguageWrapper.style.height = 'auto'; // Let video maintain aspect ratio
|
|
2103
|
-
if (resizeDirection.includes('w')) {
|
|
2104
|
-
this.signLanguageWrapper.style.left = `${newLeft}px`;
|
|
2105
|
-
}
|
|
2106
|
-
this.signLanguageWrapper.style.right = 'auto';
|
|
2107
|
-
this.signLanguageWrapper.style.bottom = 'auto';
|
|
2108
|
-
// Remove position classes
|
|
2109
|
-
this.signLanguageWrapper.classList.remove(...Array.from(this.signLanguageWrapper.classList).filter(c => c.startsWith('vidply-sign-position-')));
|
|
2110
|
-
}
|
|
2111
|
-
};
|
|
2112
|
-
|
|
2113
|
-
const onMouseUp = () => {
|
|
2114
|
-
if (isDragging || isResizing) {
|
|
2115
|
-
this.saveSignLanguagePreferences();
|
|
2116
|
-
}
|
|
2117
|
-
isDragging = false;
|
|
2118
|
-
isResizing = false;
|
|
2119
|
-
resizeDirection = null;
|
|
2120
|
-
this.signLanguageWrapper.classList.remove('vidply-sign-dragging', 'vidply-sign-resizing');
|
|
2121
|
-
};
|
|
2122
|
-
|
|
2123
|
-
// Keyboard controls
|
|
2124
|
-
const onKeyDown = (e) => {
|
|
2125
|
-
// Toggle drag mode with D key
|
|
2126
|
-
if (e.key === 'd' || e.key === 'D') {
|
|
2127
|
-
dragMode = !dragMode;
|
|
2128
|
-
resizeMode = false;
|
|
2129
|
-
this.signLanguageWrapper.classList.toggle('vidply-sign-keyboard-drag', dragMode);
|
|
2130
|
-
this.signLanguageWrapper.classList.remove('vidply-sign-keyboard-resize');
|
|
2131
|
-
e.preventDefault();
|
|
2132
|
-
return;
|
|
2133
|
-
}
|
|
2134
|
-
|
|
2135
|
-
// Toggle resize mode with R key
|
|
2136
|
-
if (e.key === 'r' || e.key === 'R') {
|
|
2137
|
-
resizeMode = !resizeMode;
|
|
2138
|
-
dragMode = false;
|
|
2139
|
-
this.signLanguageWrapper.classList.toggle('vidply-sign-keyboard-resize', resizeMode);
|
|
2140
|
-
this.signLanguageWrapper.classList.remove('vidply-sign-keyboard-drag');
|
|
2141
|
-
e.preventDefault();
|
|
2142
|
-
return;
|
|
2143
|
-
}
|
|
2144
|
-
|
|
2145
|
-
// Escape to exit modes
|
|
2146
|
-
if (e.key === 'Escape') {
|
|
2147
|
-
dragMode = false;
|
|
2148
|
-
resizeMode = false;
|
|
2149
|
-
this.signLanguageWrapper.classList.remove('vidply-sign-keyboard-drag', 'vidply-sign-keyboard-resize');
|
|
2150
|
-
e.preventDefault();
|
|
2151
|
-
return;
|
|
2152
|
-
}
|
|
2153
|
-
|
|
2154
|
-
// Arrow keys for drag/resize
|
|
2155
|
-
if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
|
|
2156
|
-
const step = e.shiftKey ? 10 : 5;
|
|
2157
|
-
const rect = this.signLanguageWrapper.getBoundingClientRect();
|
|
2158
|
-
|
|
2159
|
-
// Get videoWrapper and container bounds
|
|
2160
|
-
const videoWrapperRect = this.videoWrapper.getBoundingClientRect();
|
|
2161
|
-
const containerRect = this.container.getBoundingClientRect();
|
|
2162
|
-
|
|
2163
|
-
// Calculate videoWrapper position relative to container
|
|
2164
|
-
const videoWrapperLeft = videoWrapperRect.left - containerRect.left;
|
|
2165
|
-
const videoWrapperTop = videoWrapperRect.top - containerRect.top;
|
|
2166
|
-
|
|
2167
|
-
if (dragMode) {
|
|
2168
|
-
// Get current position relative to container
|
|
2169
|
-
let left = rect.left - containerRect.left;
|
|
2170
|
-
let top = rect.top - containerRect.top;
|
|
2171
|
-
|
|
2172
|
-
if (e.key === 'ArrowLeft') left -= step;
|
|
2173
|
-
if (e.key === 'ArrowRight') left += step;
|
|
2174
|
-
if (e.key === 'ArrowUp') top -= step;
|
|
2175
|
-
if (e.key === 'ArrowDown') top += step;
|
|
2176
|
-
|
|
2177
|
-
const controlsHeight = 95; // Height of controls when visible
|
|
2178
|
-
|
|
2179
|
-
// Constrain to videoWrapper bounds (ensuring it stays above controls)
|
|
2180
|
-
left = Math.max(videoWrapperLeft, Math.min(left, videoWrapperLeft + videoWrapperRect.width - rect.width));
|
|
2181
|
-
top = Math.max(videoWrapperTop, Math.min(top, videoWrapperTop + videoWrapperRect.height - rect.height - controlsHeight));
|
|
2182
|
-
|
|
2183
|
-
this.signLanguageWrapper.style.left = `${left}px`;
|
|
2184
|
-
this.signLanguageWrapper.style.top = `${top}px`;
|
|
2185
|
-
this.signLanguageWrapper.style.right = 'auto';
|
|
2186
|
-
this.signLanguageWrapper.style.bottom = 'auto';
|
|
2187
|
-
// Remove position classes
|
|
2188
|
-
this.signLanguageWrapper.classList.remove(...Array.from(this.signLanguageWrapper.classList).filter(c => c.startsWith('vidply-sign-position-')));
|
|
2189
|
-
this.saveSignLanguagePreferences();
|
|
2190
|
-
e.preventDefault();
|
|
2191
|
-
} else if (resizeMode) {
|
|
2192
|
-
let width = rect.width;
|
|
2193
|
-
|
|
2194
|
-
// Only adjust width, height will auto-adjust to maintain aspect ratio
|
|
2195
|
-
if (e.key === 'ArrowLeft') width -= step;
|
|
2196
|
-
if (e.key === 'ArrowRight') width += step;
|
|
2197
|
-
// Up/Down also adjusts width for simplicity
|
|
2198
|
-
if (e.key === 'ArrowUp') width += step;
|
|
2199
|
-
if (e.key === 'ArrowDown') width -= step;
|
|
2200
|
-
|
|
2201
|
-
// Constrain width
|
|
2202
|
-
width = Math.max(150, width);
|
|
2203
|
-
// Don't let it exceed videoWrapper width
|
|
2204
|
-
width = Math.min(width, videoWrapperRect.width);
|
|
2205
|
-
|
|
2206
|
-
this.signLanguageWrapper.style.width = `${width}px`;
|
|
2207
|
-
this.signLanguageWrapper.style.height = 'auto';
|
|
2208
|
-
this.saveSignLanguagePreferences();
|
|
2209
|
-
e.preventDefault();
|
|
2210
|
-
}
|
|
2211
|
-
}
|
|
2212
|
-
};
|
|
2213
|
-
|
|
2214
|
-
// Attach event listeners
|
|
2215
|
-
this.signLanguageVideo.addEventListener('mousedown', onMouseDownVideo);
|
|
2216
|
-
const handles = this.signLanguageWrapper.querySelectorAll('.vidply-sign-resize-handle');
|
|
2217
|
-
handles.forEach(handle => handle.addEventListener('mousedown', onMouseDownHandle));
|
|
2218
|
-
document.addEventListener('mousemove', onMouseMove);
|
|
2219
|
-
document.addEventListener('mouseup', onMouseUp);
|
|
2220
|
-
this.signLanguageWrapper.addEventListener('keydown', onKeyDown);
|
|
2252
|
+
// Get resize handles
|
|
2253
|
+
const resizeHandles = Array.from(this.signLanguageWrapper.querySelectorAll('.vidply-sign-resize-handle'));
|
|
2254
|
+
|
|
2255
|
+
// Create DraggableResizable utility
|
|
2256
|
+
this.signLanguageDraggable = new DraggableResizable(this.signLanguageWrapper, {
|
|
2257
|
+
dragHandle: this.signLanguageVideo,
|
|
2258
|
+
resizeHandles: resizeHandles,
|
|
2259
|
+
constrainToViewport: true,
|
|
2260
|
+
maintainAspectRatio: true,
|
|
2261
|
+
minWidth: 150,
|
|
2262
|
+
minHeight: 100,
|
|
2263
|
+
classPrefix: 'vidply-sign',
|
|
2264
|
+
keyboardDragKey: 'd',
|
|
2265
|
+
keyboardResizeKey: 'r',
|
|
2266
|
+
keyboardStep: 5,
|
|
2267
|
+
keyboardStepLarge: 10
|
|
2268
|
+
});
|
|
2221
2269
|
|
|
2222
2270
|
// Store for cleanup
|
|
2223
2271
|
this.signLanguageInteractionHandlers = {
|
|
2224
|
-
|
|
2225
|
-
mouseDownHandle: onMouseDownHandle,
|
|
2226
|
-
mouseMove: onMouseMove,
|
|
2227
|
-
mouseUp: onMouseUp,
|
|
2228
|
-
keyDown: onKeyDown,
|
|
2229
|
-
handles
|
|
2272
|
+
draggable: this.signLanguageDraggable
|
|
2230
2273
|
};
|
|
2231
2274
|
}
|
|
2232
2275
|
|
|
@@ -2317,23 +2360,14 @@ export class Player extends EventEmitter {
|
|
|
2317
2360
|
this.signLanguageHandlers = null;
|
|
2318
2361
|
}
|
|
2319
2362
|
|
|
2320
|
-
//
|
|
2321
|
-
if (this.
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
}
|
|
2325
|
-
if (this.signLanguageInteractionHandlers.handles) {
|
|
2326
|
-
this.signLanguageInteractionHandlers.handles.forEach(handle => {
|
|
2327
|
-
handle.removeEventListener('mousedown', this.signLanguageInteractionHandlers.mouseDownHandle);
|
|
2328
|
-
});
|
|
2329
|
-
}
|
|
2330
|
-
document.removeEventListener('mousemove', this.signLanguageInteractionHandlers.mouseMove);
|
|
2331
|
-
document.removeEventListener('mouseup', this.signLanguageInteractionHandlers.mouseUp);
|
|
2332
|
-
if (this.signLanguageWrapper) {
|
|
2333
|
-
this.signLanguageWrapper.removeEventListener('keydown', this.signLanguageInteractionHandlers.keyDown);
|
|
2334
|
-
}
|
|
2335
|
-
this.signLanguageInteractionHandlers = null;
|
|
2363
|
+
// Destroy draggable utility
|
|
2364
|
+
if (this.signLanguageDraggable) {
|
|
2365
|
+
this.signLanguageDraggable.destroy();
|
|
2366
|
+
this.signLanguageDraggable = null;
|
|
2336
2367
|
}
|
|
2368
|
+
|
|
2369
|
+
// Clear interaction handlers reference
|
|
2370
|
+
this.signLanguageInteractionHandlers = null;
|
|
2337
2371
|
|
|
2338
2372
|
// Remove video and wrapper elements
|
|
2339
2373
|
if (this.signLanguageWrapper && this.signLanguageWrapper.parentNode) {
|
|
@@ -2397,9 +2431,28 @@ export class Player extends EventEmitter {
|
|
|
2397
2431
|
}
|
|
2398
2432
|
|
|
2399
2433
|
// Logging
|
|
2400
|
-
log(
|
|
2401
|
-
if (this.options.debug) {
|
|
2402
|
-
|
|
2434
|
+
log(...messages) {
|
|
2435
|
+
if (!this.options.debug) {
|
|
2436
|
+
return;
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
let type = 'log';
|
|
2440
|
+
if (messages.length > 0) {
|
|
2441
|
+
const potentialType = messages[messages.length - 1];
|
|
2442
|
+
if (typeof potentialType === 'string' && console[potentialType]) {
|
|
2443
|
+
type = potentialType;
|
|
2444
|
+
messages = messages.slice(0, -1);
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
if (messages.length === 0) {
|
|
2449
|
+
messages = [''];
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
if (typeof console[type] === 'function') {
|
|
2453
|
+
console[type]('[VidPly]', ...messages);
|
|
2454
|
+
} else {
|
|
2455
|
+
console.log('[VidPly]', ...messages);
|
|
2403
2456
|
}
|
|
2404
2457
|
}
|
|
2405
2458
|
|
|
@@ -2434,7 +2487,10 @@ export class Player extends EventEmitter {
|
|
|
2434
2487
|
}
|
|
2435
2488
|
|
|
2436
2489
|
if (this.transcriptManager && this.transcriptManager.isVisible) {
|
|
2437
|
-
|
|
2490
|
+
// Only auto-position if user hasn't manually moved it
|
|
2491
|
+
if (!this.transcriptManager.draggableResizable || !this.transcriptManager.draggableResizable.manuallyPositioned) {
|
|
2492
|
+
this.transcriptManager.positionTranscript();
|
|
2493
|
+
}
|
|
2438
2494
|
}
|
|
2439
2495
|
};
|
|
2440
2496
|
|
|
@@ -2447,7 +2503,10 @@ export class Player extends EventEmitter {
|
|
|
2447
2503
|
// Wait for layout to settle
|
|
2448
2504
|
setTimeout(() => {
|
|
2449
2505
|
if (this.transcriptManager && this.transcriptManager.isVisible) {
|
|
2450
|
-
|
|
2506
|
+
// Only auto-position if user hasn't manually moved it
|
|
2507
|
+
if (!this.transcriptManager.draggableResizable || !this.transcriptManager.draggableResizable.manuallyPositioned) {
|
|
2508
|
+
this.transcriptManager.positionTranscript();
|
|
2509
|
+
}
|
|
2451
2510
|
}
|
|
2452
2511
|
}, 100);
|
|
2453
2512
|
};
|
|
@@ -2493,7 +2552,7 @@ export class Player extends EventEmitter {
|
|
|
2493
2552
|
if (this.signLanguageWrapper && this.signLanguageWrapper.style.display !== 'none') {
|
|
2494
2553
|
// Use setTimeout to ensure layout has updated after fullscreen transition
|
|
2495
2554
|
// Longer delay to account for CSS transition animations and layout recalculation
|
|
2496
|
-
|
|
2555
|
+
this.setManagedTimeout(() => {
|
|
2497
2556
|
// Use requestAnimationFrame to ensure the browser has fully rendered the layout
|
|
2498
2557
|
requestAnimationFrame(() => {
|
|
2499
2558
|
// Clear saved size and reset to default for the new container size
|
|
@@ -2580,6 +2639,29 @@ export class Player extends EventEmitter {
|
|
|
2580
2639
|
this.fullscreenChangeHandler = null;
|
|
2581
2640
|
}
|
|
2582
2641
|
|
|
2642
|
+
// Cleanup all managed timeouts
|
|
2643
|
+
this.timeouts.forEach(timeoutId => clearTimeout(timeoutId));
|
|
2644
|
+
this.timeouts.clear();
|
|
2645
|
+
|
|
2646
|
+
// Cleanup metadata handling
|
|
2647
|
+
if (this.metadataCueChangeHandler) {
|
|
2648
|
+
const textTracks = this.textTracks;
|
|
2649
|
+
const metadataTrack = textTracks.find(track => track.kind === 'metadata');
|
|
2650
|
+
if (metadataTrack) {
|
|
2651
|
+
metadataTrack.removeEventListener('cuechange', this.metadataCueChangeHandler);
|
|
2652
|
+
}
|
|
2653
|
+
this.metadataCueChangeHandler = null;
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2656
|
+
if (this.metadataAlertHandlers && this.metadataAlertHandlers.size > 0) {
|
|
2657
|
+
this.metadataAlertHandlers.forEach(({ button, handler }) => {
|
|
2658
|
+
if (button && handler) {
|
|
2659
|
+
button.removeEventListener('click', handler);
|
|
2660
|
+
}
|
|
2661
|
+
});
|
|
2662
|
+
this.metadataAlertHandlers.clear();
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2583
2665
|
// Remove container
|
|
2584
2666
|
if (this.container && this.container.parentNode) {
|
|
2585
2667
|
this.container.parentNode.insertBefore(this.element, this.container);
|
|
@@ -2588,6 +2670,424 @@ export class Player extends EventEmitter {
|
|
|
2588
2670
|
|
|
2589
2671
|
this.removeAllListeners();
|
|
2590
2672
|
}
|
|
2673
|
+
|
|
2674
|
+
/**
|
|
2675
|
+
* Setup metadata track handling
|
|
2676
|
+
* This enables metadata tracks and listens for cue changes to trigger actions
|
|
2677
|
+
*/
|
|
2678
|
+
setupMetadataHandling() {
|
|
2679
|
+
const setupMetadata = () => {
|
|
2680
|
+
const textTracks = this.textTracks;
|
|
2681
|
+
const metadataTrack = textTracks.find(track => track.kind === 'metadata');
|
|
2682
|
+
|
|
2683
|
+
if (metadataTrack) {
|
|
2684
|
+
// Enable the metadata track so cuechange events fire
|
|
2685
|
+
// Use 'hidden' mode so it doesn't display anything, but events still work
|
|
2686
|
+
if (metadataTrack.mode === 'disabled') {
|
|
2687
|
+
metadataTrack.mode = 'hidden';
|
|
2688
|
+
}
|
|
2689
|
+
|
|
2690
|
+
// Remove existing listener if any
|
|
2691
|
+
if (this.metadataCueChangeHandler) {
|
|
2692
|
+
metadataTrack.removeEventListener('cuechange', this.metadataCueChangeHandler);
|
|
2693
|
+
}
|
|
2694
|
+
|
|
2695
|
+
// Add event listener for cue changes
|
|
2696
|
+
this.metadataCueChangeHandler = () => {
|
|
2697
|
+
const activeCues = Array.from(metadataTrack.activeCues || []);
|
|
2698
|
+
if (activeCues.length > 0) {
|
|
2699
|
+
// Debug logging
|
|
2700
|
+
if (this.options.debug) {
|
|
2701
|
+
this.log('[Metadata] Active cues:', activeCues.map(c => ({
|
|
2702
|
+
start: c.startTime,
|
|
2703
|
+
end: c.endTime,
|
|
2704
|
+
text: c.text
|
|
2705
|
+
})));
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
activeCues.forEach(cue => {
|
|
2709
|
+
this.handleMetadataCue(cue);
|
|
2710
|
+
});
|
|
2711
|
+
};
|
|
2712
|
+
|
|
2713
|
+
metadataTrack.addEventListener('cuechange', this.metadataCueChangeHandler);
|
|
2714
|
+
|
|
2715
|
+
// Debug: Log metadata track setup
|
|
2716
|
+
if (this.options.debug) {
|
|
2717
|
+
const cueCount = metadataTrack.cues ? metadataTrack.cues.length : 0;
|
|
2718
|
+
this.log('[Metadata] Track enabled,', cueCount, 'cues available');
|
|
2719
|
+
}
|
|
2720
|
+
} else if (this.options.debug) {
|
|
2721
|
+
this.log('[Metadata] No metadata track found');
|
|
2722
|
+
}
|
|
2723
|
+
};
|
|
2724
|
+
|
|
2725
|
+
// Try immediately
|
|
2726
|
+
setupMetadata();
|
|
2727
|
+
|
|
2728
|
+
// Also try after loadedmetadata event (tracks might not be ready yet)
|
|
2729
|
+
this.on('loadedmetadata', setupMetadata);
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
normalizeMetadataSelector(selector) {
|
|
2733
|
+
if (!selector) {
|
|
2734
|
+
return null;
|
|
2735
|
+
}
|
|
2736
|
+
const trimmed = selector.trim();
|
|
2737
|
+
if (!trimmed) {
|
|
2738
|
+
return null;
|
|
2739
|
+
}
|
|
2740
|
+
if (trimmed.startsWith('#') || trimmed.startsWith('.') || trimmed.startsWith('[')) {
|
|
2741
|
+
return trimmed;
|
|
2742
|
+
}
|
|
2743
|
+
return `#${trimmed}`;
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2746
|
+
resolveMetadataConfig(map, key) {
|
|
2747
|
+
if (!map || !key) {
|
|
2748
|
+
return null;
|
|
2749
|
+
}
|
|
2750
|
+
if (Object.prototype.hasOwnProperty.call(map, key)) {
|
|
2751
|
+
return map[key];
|
|
2752
|
+
}
|
|
2753
|
+
const withoutHash = key.replace(/^#/, '');
|
|
2754
|
+
if (Object.prototype.hasOwnProperty.call(map, withoutHash)) {
|
|
2755
|
+
return map[withoutHash];
|
|
2756
|
+
}
|
|
2757
|
+
return null;
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
cacheMetadataAlertContent(element, config = {}) {
|
|
2761
|
+
if (!element) {
|
|
2762
|
+
return;
|
|
2763
|
+
}
|
|
2764
|
+
const titleSelector = config.titleSelector || '[data-vidply-alert-title], h3, header';
|
|
2765
|
+
const messageSelector = config.messageSelector || '[data-vidply-alert-message], p';
|
|
2766
|
+
|
|
2767
|
+
const titleEl = element.querySelector(titleSelector);
|
|
2768
|
+
if (titleEl && !titleEl.dataset.vidplyAlertTitleOriginal) {
|
|
2769
|
+
titleEl.dataset.vidplyAlertTitleOriginal = titleEl.textContent.trim();
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
const messageEl = element.querySelector(messageSelector);
|
|
2773
|
+
if (messageEl && !messageEl.dataset.vidplyAlertMessageOriginal) {
|
|
2774
|
+
messageEl.dataset.vidplyAlertMessageOriginal = messageEl.textContent.trim();
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2778
|
+
restoreMetadataAlertContent(element, config = {}) {
|
|
2779
|
+
if (!element) {
|
|
2780
|
+
return;
|
|
2781
|
+
}
|
|
2782
|
+
const titleSelector = config.titleSelector || '[data-vidply-alert-title], h3, header';
|
|
2783
|
+
const messageSelector = config.messageSelector || '[data-vidply-alert-message], p';
|
|
2784
|
+
|
|
2785
|
+
const titleEl = element.querySelector(titleSelector);
|
|
2786
|
+
if (titleEl && titleEl.dataset.vidplyAlertTitleOriginal) {
|
|
2787
|
+
titleEl.textContent = titleEl.dataset.vidplyAlertTitleOriginal;
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2790
|
+
const messageEl = element.querySelector(messageSelector);
|
|
2791
|
+
if (messageEl && messageEl.dataset.vidplyAlertMessageOriginal) {
|
|
2792
|
+
messageEl.textContent = messageEl.dataset.vidplyAlertMessageOriginal;
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2796
|
+
focusMetadataTarget(target, fallbackElement = null) {
|
|
2797
|
+
if (!target || target === 'none') {
|
|
2798
|
+
return;
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2801
|
+
if (target === 'alert' && fallbackElement) {
|
|
2802
|
+
fallbackElement.focus();
|
|
2803
|
+
return;
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2806
|
+
if (target === 'player') {
|
|
2807
|
+
if (this.container) {
|
|
2808
|
+
this.container.focus();
|
|
2809
|
+
}
|
|
2810
|
+
return;
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
if (target === 'media') {
|
|
2814
|
+
this.element.focus();
|
|
2815
|
+
return;
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2818
|
+
if (target === 'playButton') {
|
|
2819
|
+
const playButton = this.controlBar?.controls?.playPause;
|
|
2820
|
+
if (playButton) {
|
|
2821
|
+
playButton.focus();
|
|
2822
|
+
}
|
|
2823
|
+
return;
|
|
2824
|
+
}
|
|
2825
|
+
|
|
2826
|
+
if (typeof target === 'string') {
|
|
2827
|
+
const targetElement = document.querySelector(target);
|
|
2828
|
+
if (targetElement) {
|
|
2829
|
+
if (targetElement.tabIndex === -1 && !targetElement.hasAttribute('tabindex')) {
|
|
2830
|
+
targetElement.setAttribute('tabindex', '-1');
|
|
2831
|
+
}
|
|
2832
|
+
targetElement.focus();
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
|
|
2837
|
+
handleMetadataAlert(selector, options = {}) {
|
|
2838
|
+
if (!selector) {
|
|
2839
|
+
return;
|
|
2840
|
+
}
|
|
2841
|
+
|
|
2842
|
+
const config = this.resolveMetadataConfig(this.options.metadataAlerts, selector) || {};
|
|
2843
|
+
const element = options.element || document.querySelector(selector);
|
|
2844
|
+
|
|
2845
|
+
if (!element) {
|
|
2846
|
+
if (this.options.debug) {
|
|
2847
|
+
this.log('[Metadata] Alert element not found:', selector);
|
|
2848
|
+
}
|
|
2849
|
+
return;
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
if (this.options.debug) {
|
|
2853
|
+
this.log('[Metadata] Handling alert', selector, { reason: options.reason, config });
|
|
2854
|
+
}
|
|
2855
|
+
|
|
2856
|
+
this.cacheMetadataAlertContent(element, config);
|
|
2857
|
+
|
|
2858
|
+
if (!element.dataset.vidplyAlertOriginalDisplay) {
|
|
2859
|
+
element.dataset.vidplyAlertOriginalDisplay = element.style.display || '';
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2862
|
+
if (!element.dataset.vidplyAlertDisplay) {
|
|
2863
|
+
element.dataset.vidplyAlertDisplay = config.display || 'block';
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
const shouldShow = options.show !== undefined ? options.show : (config.show !== false);
|
|
2867
|
+
if (shouldShow) {
|
|
2868
|
+
const displayValue = config.display || element.dataset.vidplyAlertDisplay || 'block';
|
|
2869
|
+
element.style.display = displayValue;
|
|
2870
|
+
element.hidden = false;
|
|
2871
|
+
element.removeAttribute('hidden');
|
|
2872
|
+
element.setAttribute('aria-hidden', 'false');
|
|
2873
|
+
element.setAttribute('data-vidply-alert-active', 'true');
|
|
2874
|
+
}
|
|
2875
|
+
|
|
2876
|
+
const shouldReset = config.resetContent !== false && options.reason === 'focus';
|
|
2877
|
+
if (shouldReset) {
|
|
2878
|
+
this.restoreMetadataAlertContent(element, config);
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
const shouldFocus = options.focus !== undefined
|
|
2882
|
+
? options.focus
|
|
2883
|
+
: (config.focusOnShow ?? (options.reason !== 'focus'));
|
|
2884
|
+
|
|
2885
|
+
if (shouldShow && shouldFocus) {
|
|
2886
|
+
if (element.tabIndex === -1 && !element.hasAttribute('tabindex')) {
|
|
2887
|
+
element.setAttribute('tabindex', '-1');
|
|
2888
|
+
}
|
|
2889
|
+
element.focus();
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
if (shouldShow && config.autoScroll !== false && options.autoScroll !== false) {
|
|
2893
|
+
element.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2896
|
+
const continueSelector = config.continueButton;
|
|
2897
|
+
if (continueSelector) {
|
|
2898
|
+
let continueButton = null;
|
|
2899
|
+
if (continueSelector === 'self') {
|
|
2900
|
+
continueButton = element;
|
|
2901
|
+
} else if (element.matches(continueSelector)) {
|
|
2902
|
+
continueButton = element;
|
|
2903
|
+
} else {
|
|
2904
|
+
continueButton = element.querySelector(continueSelector) || document.querySelector(continueSelector);
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
if (continueButton && !this.metadataAlertHandlers.has(selector)) {
|
|
2908
|
+
const handler = () => {
|
|
2909
|
+
const hideOnContinue = config.hideOnContinue !== false;
|
|
2910
|
+
if (hideOnContinue) {
|
|
2911
|
+
const originalDisplay = element.dataset.vidplyAlertOriginalDisplay || '';
|
|
2912
|
+
element.style.display = config.hideDisplay || originalDisplay || 'none';
|
|
2913
|
+
element.setAttribute('aria-hidden', 'true');
|
|
2914
|
+
element.removeAttribute('data-vidply-alert-active');
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
if (config.resume !== false && this.state.paused) {
|
|
2918
|
+
this.play();
|
|
2919
|
+
}
|
|
2920
|
+
|
|
2921
|
+
const focusTarget = config.focusTarget || 'playButton';
|
|
2922
|
+
this.setManagedTimeout(() => {
|
|
2923
|
+
this.focusMetadataTarget(focusTarget, element);
|
|
2924
|
+
}, config.focusDelay ?? 100);
|
|
2925
|
+
};
|
|
2926
|
+
|
|
2927
|
+
continueButton.addEventListener('click', handler);
|
|
2928
|
+
this.metadataAlertHandlers.set(selector, { button: continueButton, handler });
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
|
|
2932
|
+
return element;
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2935
|
+
handleMetadataHashtags(hashtags) {
|
|
2936
|
+
if (!Array.isArray(hashtags) || hashtags.length === 0) {
|
|
2937
|
+
return;
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2940
|
+
const configMap = this.options.metadataHashtags;
|
|
2941
|
+
if (!configMap) {
|
|
2942
|
+
return;
|
|
2943
|
+
}
|
|
2944
|
+
|
|
2945
|
+
hashtags.forEach(tag => {
|
|
2946
|
+
const config = this.resolveMetadataConfig(configMap, tag);
|
|
2947
|
+
if (!config) {
|
|
2948
|
+
return;
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2951
|
+
const selector = this.normalizeMetadataSelector(config.alert || config.selector || config.target);
|
|
2952
|
+
if (!selector) {
|
|
2953
|
+
return;
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2956
|
+
const element = document.querySelector(selector);
|
|
2957
|
+
if (!element) {
|
|
2958
|
+
if (this.options.debug) {
|
|
2959
|
+
this.log('[Metadata] Hashtag target not found:', selector);
|
|
2960
|
+
}
|
|
2961
|
+
return;
|
|
2962
|
+
}
|
|
2963
|
+
|
|
2964
|
+
if (this.options.debug) {
|
|
2965
|
+
this.log('[Metadata] Handling hashtag', tag, { selector, config });
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
this.cacheMetadataAlertContent(element, config);
|
|
2969
|
+
|
|
2970
|
+
if (config.title) {
|
|
2971
|
+
const titleSelector = config.titleSelector || '[data-vidply-alert-title], h3, header';
|
|
2972
|
+
const titleEl = element.querySelector(titleSelector);
|
|
2973
|
+
if (titleEl) {
|
|
2974
|
+
titleEl.textContent = config.title;
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
|
|
2978
|
+
if (config.message) {
|
|
2979
|
+
const messageSelector = config.messageSelector || '[data-vidply-alert-message], p';
|
|
2980
|
+
const messageEl = element.querySelector(messageSelector);
|
|
2981
|
+
if (messageEl) {
|
|
2982
|
+
messageEl.textContent = config.message;
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
|
|
2986
|
+
const show = config.show !== false;
|
|
2987
|
+
const focus = config.focus !== undefined ? config.focus : false;
|
|
2988
|
+
|
|
2989
|
+
this.handleMetadataAlert(selector, {
|
|
2990
|
+
element,
|
|
2991
|
+
show,
|
|
2992
|
+
focus,
|
|
2993
|
+
autoScroll: config.autoScroll,
|
|
2994
|
+
reason: 'hashtag'
|
|
2995
|
+
});
|
|
2996
|
+
});
|
|
2997
|
+
}
|
|
2998
|
+
|
|
2999
|
+
/**
|
|
3000
|
+
* Handle individual metadata cues
|
|
3001
|
+
* Parses metadata text and emits events or triggers actions
|
|
3002
|
+
*/
|
|
3003
|
+
handleMetadataCue(cue) {
|
|
3004
|
+
const text = cue.text.trim();
|
|
3005
|
+
|
|
3006
|
+
// Debug logging
|
|
3007
|
+
if (this.options.debug) {
|
|
3008
|
+
this.log('[Metadata] Processing cue:', {
|
|
3009
|
+
time: cue.startTime,
|
|
3010
|
+
text: text
|
|
3011
|
+
});
|
|
3012
|
+
}
|
|
3013
|
+
|
|
3014
|
+
// Emit a generic metadata event that developers can listen to
|
|
3015
|
+
this.emit('metadata', {
|
|
3016
|
+
time: cue.startTime,
|
|
3017
|
+
endTime: cue.endTime,
|
|
3018
|
+
text: text,
|
|
3019
|
+
cue: cue
|
|
3020
|
+
});
|
|
3021
|
+
|
|
3022
|
+
// Parse for specific commands (examples based on wwa_meta.vtt format)
|
|
3023
|
+
if (text.includes('PAUSE')) {
|
|
3024
|
+
// Automatically pause the video
|
|
3025
|
+
if (!this.state.paused) {
|
|
3026
|
+
if (this.options.debug) {
|
|
3027
|
+
this.log('[Metadata] Pausing video at', cue.startTime);
|
|
3028
|
+
}
|
|
3029
|
+
this.pause();
|
|
3030
|
+
}
|
|
3031
|
+
// Also emit event for developers who want to listen
|
|
3032
|
+
this.emit('metadata:pause', { time: cue.startTime, text: text });
|
|
3033
|
+
}
|
|
3034
|
+
|
|
3035
|
+
// Parse for focus directives
|
|
3036
|
+
const focusMatch = text.match(/FOCUS:([\w#-]+)/);
|
|
3037
|
+
if (focusMatch) {
|
|
3038
|
+
const targetSelector = focusMatch[1];
|
|
3039
|
+
const normalizedSelector = this.normalizeMetadataSelector(targetSelector);
|
|
3040
|
+
// Automatically focus the target element
|
|
3041
|
+
const targetElement = normalizedSelector ? document.querySelector(normalizedSelector) : null;
|
|
3042
|
+
if (targetElement) {
|
|
3043
|
+
if (this.options.debug) {
|
|
3044
|
+
this.log('[Metadata] Focusing element:', normalizedSelector);
|
|
3045
|
+
}
|
|
3046
|
+
// Make element focusable if it isn't already
|
|
3047
|
+
if (targetElement.tabIndex === -1 && !targetElement.hasAttribute('tabindex')) {
|
|
3048
|
+
targetElement.setAttribute('tabindex', '-1');
|
|
3049
|
+
}
|
|
3050
|
+
// Use setTimeout to ensure DOM is ready
|
|
3051
|
+
this.setManagedTimeout(() => {
|
|
3052
|
+
targetElement.focus();
|
|
3053
|
+
// Scroll element into view if needed
|
|
3054
|
+
targetElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
3055
|
+
}, 10);
|
|
3056
|
+
} else if (this.options.debug) {
|
|
3057
|
+
this.log('[Metadata] Element not found:', normalizedSelector || targetSelector);
|
|
3058
|
+
}
|
|
3059
|
+
// Also emit event for developers who want to listen
|
|
3060
|
+
this.emit('metadata:focus', {
|
|
3061
|
+
time: cue.startTime,
|
|
3062
|
+
target: targetSelector,
|
|
3063
|
+
selector: normalizedSelector,
|
|
3064
|
+
element: targetElement,
|
|
3065
|
+
text: text
|
|
3066
|
+
});
|
|
3067
|
+
|
|
3068
|
+
if (normalizedSelector) {
|
|
3069
|
+
this.handleMetadataAlert(normalizedSelector, {
|
|
3070
|
+
element: targetElement,
|
|
3071
|
+
reason: 'focus'
|
|
3072
|
+
});
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
|
|
3076
|
+
// Parse for hashtag references
|
|
3077
|
+
const hashtags = text.match(/#[\w-]+/g);
|
|
3078
|
+
if (hashtags) {
|
|
3079
|
+
if (this.options.debug) {
|
|
3080
|
+
this.log('[Metadata] Hashtags found:', hashtags);
|
|
3081
|
+
}
|
|
3082
|
+
this.emit('metadata:hashtags', {
|
|
3083
|
+
time: cue.startTime,
|
|
3084
|
+
hashtags: hashtags,
|
|
3085
|
+
text: text
|
|
3086
|
+
});
|
|
3087
|
+
|
|
3088
|
+
this.handleMetadataHashtags(hashtags);
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
2591
3091
|
}
|
|
2592
3092
|
|
|
2593
3093
|
// Static instances tracker for pause others functionality
|