vidply 1.0.7 → 1.0.8
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.esm.js +683 -6
- package/dist/vidply.esm.js.map +3 -3
- package/dist/vidply.esm.min.js +6 -6
- package/dist/vidply.esm.min.meta.json +3 -3
- package/dist/vidply.js +683 -6
- package/dist/vidply.js.map +3 -3
- package/dist/vidply.min.js +6 -6
- package/dist/vidply.min.meta.json +3 -3
- package/package.json +2 -2
- package/src/core/Player.js +988 -9
package/src/core/Player.js
CHANGED
|
@@ -204,6 +204,11 @@ export class Player extends EventEmitter {
|
|
|
204
204
|
this.audioDescriptionSrc = this.options.audioDescriptionSrc;
|
|
205
205
|
this.signLanguageSrc = this.options.signLanguageSrc;
|
|
206
206
|
this.signLanguageVideo = null;
|
|
207
|
+
// Store references to source elements with audio description attributes
|
|
208
|
+
this.audioDescriptionSourceElement = null;
|
|
209
|
+
this.originalAudioDescriptionSource = null;
|
|
210
|
+
// Store caption tracks that should be swapped for audio description
|
|
211
|
+
this.audioDescriptionCaptionTracks = [];
|
|
207
212
|
|
|
208
213
|
// Components
|
|
209
214
|
this.container = null;
|
|
@@ -441,7 +446,77 @@ export class Player extends EventEmitter {
|
|
|
441
446
|
throw new Error('No media source found');
|
|
442
447
|
}
|
|
443
448
|
|
|
444
|
-
//
|
|
449
|
+
// Check for source elements with audio description attributes
|
|
450
|
+
const sourceElements = this.element.querySelectorAll('source');
|
|
451
|
+
for (const sourceEl of sourceElements) {
|
|
452
|
+
const descSrc = sourceEl.getAttribute('data-desc-src');
|
|
453
|
+
const origSrc = sourceEl.getAttribute('data-orig-src');
|
|
454
|
+
|
|
455
|
+
if (descSrc || origSrc) {
|
|
456
|
+
// Found a source element with audio description attributes
|
|
457
|
+
// Store the first one as reference, but we'll search all of them when toggling
|
|
458
|
+
if (!this.audioDescriptionSourceElement) {
|
|
459
|
+
this.audioDescriptionSourceElement = sourceEl;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (origSrc) {
|
|
463
|
+
// Store the original src from the attribute for this source
|
|
464
|
+
if (!this.originalAudioDescriptionSource) {
|
|
465
|
+
this.originalAudioDescriptionSource = origSrc;
|
|
466
|
+
}
|
|
467
|
+
// Store the original src from the first source element that has data-orig-src
|
|
468
|
+
if (!this.originalSrc) {
|
|
469
|
+
this.originalSrc = origSrc;
|
|
470
|
+
}
|
|
471
|
+
} else {
|
|
472
|
+
// If data-orig-src is not set, use the current src attribute
|
|
473
|
+
const currentSrcAttr = sourceEl.getAttribute('src');
|
|
474
|
+
if (!this.originalAudioDescriptionSource && currentSrcAttr) {
|
|
475
|
+
this.originalAudioDescriptionSource = currentSrcAttr;
|
|
476
|
+
}
|
|
477
|
+
if (!this.originalSrc && currentSrcAttr) {
|
|
478
|
+
this.originalSrc = currentSrcAttr;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Store audio description source from data-desc-src (use first one found)
|
|
483
|
+
if (descSrc && !this.audioDescriptionSrc) {
|
|
484
|
+
this.audioDescriptionSrc = descSrc;
|
|
485
|
+
}
|
|
486
|
+
// Continue checking all source elements to ensure we capture all audio description sources
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Check for caption/subtitle tracks with audio description versions
|
|
491
|
+
// Only tracks with explicit data-desc-src attribute are swapped (no auto-detection to avoid 404 errors)
|
|
492
|
+
// Description tracks (kind="descriptions") are NOT swapped - they're for transcripts
|
|
493
|
+
const trackElements = this.element.querySelectorAll('track');
|
|
494
|
+
trackElements.forEach(trackEl => {
|
|
495
|
+
const trackKind = trackEl.getAttribute('kind');
|
|
496
|
+
const trackDescSrc = trackEl.getAttribute('data-desc-src');
|
|
497
|
+
|
|
498
|
+
// Only handle caption/subtitle tracks (not description tracks)
|
|
499
|
+
// Description tracks stay as-is since they're for transcripts
|
|
500
|
+
// Include captions, subtitles, and chapters tracks that can be swapped for audio description
|
|
501
|
+
if (trackKind === 'captions' || trackKind === 'subtitles' || trackKind === 'chapters') {
|
|
502
|
+
if (trackDescSrc) {
|
|
503
|
+
// Found a track with explicit data-desc-src - this is the described version
|
|
504
|
+
this.audioDescriptionCaptionTracks.push({
|
|
505
|
+
trackElement: trackEl,
|
|
506
|
+
originalSrc: trackEl.getAttribute('src'),
|
|
507
|
+
describedSrc: trackDescSrc,
|
|
508
|
+
originalTrackSrc: trackEl.getAttribute('data-orig-src') || trackEl.getAttribute('src'),
|
|
509
|
+
explicit: true // Explicitly defined, so we should validate it
|
|
510
|
+
});
|
|
511
|
+
this.log(`Found explicit described ${trackKind} track: ${trackEl.getAttribute('src')} -> ${trackDescSrc}`);
|
|
512
|
+
}
|
|
513
|
+
// Note: Auto-detection disabled to avoid 404 console errors
|
|
514
|
+
// If you want described tracks, add data-desc-src attribute to the track element
|
|
515
|
+
}
|
|
516
|
+
// Description tracks (kind="descriptions") are ignored - they remain unchanged for transcripts
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
// Store original source for audio description toggling (fallback if not set above)
|
|
445
520
|
if (!this.originalSrc) {
|
|
446
521
|
this.originalSrc = src;
|
|
447
522
|
}
|
|
@@ -803,10 +878,28 @@ export class Player extends EventEmitter {
|
|
|
803
878
|
}
|
|
804
879
|
}
|
|
805
880
|
|
|
881
|
+
/**
|
|
882
|
+
* Check if a track file exists
|
|
883
|
+
* @param {string} url - Track file URL
|
|
884
|
+
* @returns {Promise<boolean>} - True if file exists
|
|
885
|
+
*/
|
|
886
|
+
async validateTrackExists(url) {
|
|
887
|
+
try {
|
|
888
|
+
const response = await fetch(url, { method: 'HEAD', cache: 'no-cache' });
|
|
889
|
+
return response.ok;
|
|
890
|
+
} catch (error) {
|
|
891
|
+
return false;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
806
895
|
// Audio Description
|
|
807
896
|
async enableAudioDescription() {
|
|
808
|
-
if (
|
|
809
|
-
|
|
897
|
+
// Check if we have source elements with data-desc-src (even if audioDescriptionSrc is not set)
|
|
898
|
+
const hasSourceElementsWithDesc = Array.from(this.element.querySelectorAll('source')).some(el => el.getAttribute('data-desc-src'));
|
|
899
|
+
const hasTracksWithDesc = this.audioDescriptionCaptionTracks.length > 0;
|
|
900
|
+
|
|
901
|
+
if (!this.audioDescriptionSrc && !hasSourceElementsWithDesc && !hasTracksWithDesc) {
|
|
902
|
+
console.warn('VidPly: No audio description source, source elements, or tracks provided');
|
|
810
903
|
return;
|
|
811
904
|
}
|
|
812
905
|
|
|
@@ -814,8 +907,537 @@ export class Player extends EventEmitter {
|
|
|
814
907
|
const currentTime = this.state.currentTime;
|
|
815
908
|
const wasPlaying = this.state.playing;
|
|
816
909
|
|
|
910
|
+
// Store swapped tracks for transcript reload (declare at function scope)
|
|
911
|
+
let swappedTracksForTranscript = [];
|
|
912
|
+
|
|
817
913
|
// Switch to audio-described version
|
|
818
|
-
|
|
914
|
+
// If we have a source element with audio description attributes, update that instead
|
|
915
|
+
if (this.audioDescriptionSourceElement) {
|
|
916
|
+
const currentSrc = this.element.currentSrc || this.element.src;
|
|
917
|
+
|
|
918
|
+
// Find the source element that matches the currently active source
|
|
919
|
+
const sourceElements = Array.from(this.element.querySelectorAll('source'));
|
|
920
|
+
let sourceElementToUpdate = null;
|
|
921
|
+
let descSrc = this.audioDescriptionSrc;
|
|
922
|
+
|
|
923
|
+
for (const sourceEl of sourceElements) {
|
|
924
|
+
const sourceSrc = sourceEl.getAttribute('src');
|
|
925
|
+
const descSrcAttr = sourceEl.getAttribute('data-desc-src');
|
|
926
|
+
|
|
927
|
+
// Check if this source matches the current source (by filename)
|
|
928
|
+
// Match by full path or just filename
|
|
929
|
+
const sourceFilename = sourceSrc ? sourceSrc.split('/').pop() : '';
|
|
930
|
+
const currentFilename = currentSrc ? currentSrc.split('/').pop() : '';
|
|
931
|
+
|
|
932
|
+
if (currentSrc && (currentSrc === sourceSrc ||
|
|
933
|
+
currentSrc.includes(sourceSrc) ||
|
|
934
|
+
currentSrc.includes(sourceFilename) ||
|
|
935
|
+
(sourceFilename && currentFilename === sourceFilename))) {
|
|
936
|
+
sourceElementToUpdate = sourceEl;
|
|
937
|
+
if (descSrcAttr) {
|
|
938
|
+
descSrc = descSrcAttr;
|
|
939
|
+
} else if (sourceSrc) {
|
|
940
|
+
// If no data-desc-src, try to construct it from the source
|
|
941
|
+
// But prefer the stored audioDescriptionSrc if available
|
|
942
|
+
descSrc = this.audioDescriptionSrc || descSrc;
|
|
943
|
+
}
|
|
944
|
+
break;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// If we didn't find a match, use the stored source element
|
|
949
|
+
if (!sourceElementToUpdate) {
|
|
950
|
+
sourceElementToUpdate = this.audioDescriptionSourceElement;
|
|
951
|
+
// Ensure we have the correct descSrc from the stored element
|
|
952
|
+
const storedDescSrc = sourceElementToUpdate.getAttribute('data-desc-src');
|
|
953
|
+
if (storedDescSrc) {
|
|
954
|
+
descSrc = storedDescSrc;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// Swap caption tracks to described versions BEFORE loading
|
|
959
|
+
if (this.audioDescriptionCaptionTracks.length > 0) {
|
|
960
|
+
// Swap tracks: validate explicit tracks, but try auto-detected tracks without validation
|
|
961
|
+
// This avoids 404 errors while still allowing auto-detection to work
|
|
962
|
+
const validationPromises = this.audioDescriptionCaptionTracks.map(async (trackInfo) => {
|
|
963
|
+
if (trackInfo.trackElement && trackInfo.describedSrc) {
|
|
964
|
+
// Only validate explicitly defined tracks (to confirm they exist)
|
|
965
|
+
// Auto-detected tracks are used without validation (browser will handle missing files gracefully)
|
|
966
|
+
if (trackInfo.explicit === true) {
|
|
967
|
+
try {
|
|
968
|
+
const exists = await this.validateTrackExists(trackInfo.describedSrc);
|
|
969
|
+
return { trackInfo, exists };
|
|
970
|
+
} catch (error) {
|
|
971
|
+
// Silently handle validation errors
|
|
972
|
+
return { trackInfo, exists: false };
|
|
973
|
+
}
|
|
974
|
+
} else {
|
|
975
|
+
// This shouldn't happen since auto-detection is disabled
|
|
976
|
+
// But if it does, don't validate to avoid 404s
|
|
977
|
+
return { trackInfo, exists: false };
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
return { trackInfo, exists: false };
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
const validationResults = await Promise.all(validationPromises);
|
|
984
|
+
const tracksToSwap = validationResults.filter(result => result.exists);
|
|
985
|
+
|
|
986
|
+
if (tracksToSwap.length > 0) {
|
|
987
|
+
// Store original track modes before removing tracks
|
|
988
|
+
const trackModes = new Map();
|
|
989
|
+
tracksToSwap.forEach(({ trackInfo }) => {
|
|
990
|
+
const textTrack = trackInfo.trackElement.track;
|
|
991
|
+
if (textTrack) {
|
|
992
|
+
trackModes.set(trackInfo, {
|
|
993
|
+
wasShowing: textTrack.mode === 'showing',
|
|
994
|
+
wasHidden: textTrack.mode === 'hidden'
|
|
995
|
+
});
|
|
996
|
+
} else {
|
|
997
|
+
trackModes.set(trackInfo, {
|
|
998
|
+
wasShowing: false,
|
|
999
|
+
wasHidden: false
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
// Store all track information before removing
|
|
1005
|
+
const tracksToReadd = tracksToSwap.map(({ trackInfo }) => {
|
|
1006
|
+
const oldSrc = trackInfo.trackElement.getAttribute('src');
|
|
1007
|
+
const parent = trackInfo.trackElement.parentNode;
|
|
1008
|
+
const nextSibling = trackInfo.trackElement.nextSibling;
|
|
1009
|
+
|
|
1010
|
+
// Store all attributes from the old track
|
|
1011
|
+
const attributes = {};
|
|
1012
|
+
Array.from(trackInfo.trackElement.attributes).forEach(attr => {
|
|
1013
|
+
attributes[attr.name] = attr.value;
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
return {
|
|
1017
|
+
trackInfo,
|
|
1018
|
+
oldSrc,
|
|
1019
|
+
parent,
|
|
1020
|
+
nextSibling,
|
|
1021
|
+
attributes
|
|
1022
|
+
};
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
// Remove ALL old tracks first to force browser to clear TextTrack objects
|
|
1026
|
+
tracksToReadd.forEach(({ trackInfo }) => {
|
|
1027
|
+
trackInfo.trackElement.remove();
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
// Force browser to process the removal by calling load()
|
|
1031
|
+
this.element.load();
|
|
1032
|
+
|
|
1033
|
+
// Wait for browser to process the removal, then add new tracks
|
|
1034
|
+
setTimeout(() => {
|
|
1035
|
+
tracksToReadd.forEach(({ trackInfo, oldSrc, parent, nextSibling, attributes }) => {
|
|
1036
|
+
swappedTracksForTranscript.push(trackInfo);
|
|
1037
|
+
|
|
1038
|
+
// Create a completely new track element (not a clone) to force browser to create new TextTrack
|
|
1039
|
+
const newTrackElement = document.createElement('track');
|
|
1040
|
+
newTrackElement.setAttribute('src', trackInfo.describedSrc);
|
|
1041
|
+
|
|
1042
|
+
// Copy all attributes except src and data-desc-src
|
|
1043
|
+
Object.keys(attributes).forEach(attrName => {
|
|
1044
|
+
if (attrName !== 'src' && attrName !== 'data-desc-src') {
|
|
1045
|
+
newTrackElement.setAttribute(attrName, attributes[attrName]);
|
|
1046
|
+
}
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
// Insert new track element
|
|
1050
|
+
if (nextSibling && nextSibling.parentNode) {
|
|
1051
|
+
parent.insertBefore(newTrackElement, nextSibling);
|
|
1052
|
+
} else {
|
|
1053
|
+
parent.appendChild(newTrackElement);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// Update reference to the new track element
|
|
1057
|
+
trackInfo.trackElement = newTrackElement;
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
// After all new tracks are added, force browser to reload media element again
|
|
1061
|
+
// This ensures new track elements are processed and new TextTrack objects are created
|
|
1062
|
+
this.element.load();
|
|
1063
|
+
|
|
1064
|
+
// Wait for loadedmetadata event before accessing new TextTrack objects
|
|
1065
|
+
const setupNewTracks = () => {
|
|
1066
|
+
// Wait a bit more for browser to fully process the new track elements
|
|
1067
|
+
setTimeout(() => {
|
|
1068
|
+
swappedTracksForTranscript.forEach((trackInfo) => {
|
|
1069
|
+
const trackElement = trackInfo.trackElement;
|
|
1070
|
+
const newTextTrack = trackElement.track;
|
|
1071
|
+
|
|
1072
|
+
if (newTextTrack) {
|
|
1073
|
+
// Get original mode from stored map
|
|
1074
|
+
const modeInfo = trackModes.get(trackInfo) || { wasShowing: false, wasHidden: false };
|
|
1075
|
+
|
|
1076
|
+
// Set mode to load the new track
|
|
1077
|
+
newTextTrack.mode = 'hidden'; // Use hidden to load cues without showing
|
|
1078
|
+
|
|
1079
|
+
// Restore original mode after track loads
|
|
1080
|
+
// Note: CaptionManager will handle enabling captions separately
|
|
1081
|
+
const restoreMode = () => {
|
|
1082
|
+
if (modeInfo.wasShowing) {
|
|
1083
|
+
// Set to hidden - CaptionManager will set it to showing when it enables
|
|
1084
|
+
newTextTrack.mode = 'hidden';
|
|
1085
|
+
} else if (modeInfo.wasHidden) {
|
|
1086
|
+
newTextTrack.mode = 'hidden';
|
|
1087
|
+
} else {
|
|
1088
|
+
newTextTrack.mode = 'disabled';
|
|
1089
|
+
}
|
|
1090
|
+
};
|
|
1091
|
+
|
|
1092
|
+
// Wait for track to load
|
|
1093
|
+
if (newTextTrack.readyState >= 2) { // LOADED
|
|
1094
|
+
restoreMode();
|
|
1095
|
+
} else {
|
|
1096
|
+
newTextTrack.addEventListener('load', restoreMode, { once: true });
|
|
1097
|
+
newTextTrack.addEventListener('error', restoreMode, { once: true });
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
});
|
|
1101
|
+
}, 300); // Additional wait for browser to process track elements
|
|
1102
|
+
};
|
|
1103
|
+
|
|
1104
|
+
// Wait for loadedmetadata event which fires when browser processes track elements
|
|
1105
|
+
if (this.element.readyState >= 1) { // HAVE_METADATA
|
|
1106
|
+
// Already loaded, wait a bit and setup
|
|
1107
|
+
setTimeout(setupNewTracks, 200);
|
|
1108
|
+
} else {
|
|
1109
|
+
this.element.addEventListener('loadedmetadata', setupNewTracks, { once: true });
|
|
1110
|
+
// Fallback timeout
|
|
1111
|
+
setTimeout(setupNewTracks, 2000);
|
|
1112
|
+
}
|
|
1113
|
+
}, 100); // Wait 100ms after first load() before adding new tracks
|
|
1114
|
+
|
|
1115
|
+
const skippedCount = validationResults.length - tracksToSwap.length;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// Update all source elements that have data-desc-src to their described versions
|
|
1120
|
+
// Force browser to pick up changes by removing and re-adding source elements
|
|
1121
|
+
// Get source elements (may have been defined in if block above, but get fresh list here)
|
|
1122
|
+
const allSourceElements = Array.from(this.element.querySelectorAll('source'));
|
|
1123
|
+
const sourcesToUpdate = [];
|
|
1124
|
+
|
|
1125
|
+
allSourceElements.forEach((sourceEl) => {
|
|
1126
|
+
const descSrcAttr = sourceEl.getAttribute('data-desc-src');
|
|
1127
|
+
const currentSrc = sourceEl.getAttribute('src');
|
|
1128
|
+
|
|
1129
|
+
if (descSrcAttr) {
|
|
1130
|
+
const type = sourceEl.getAttribute('type');
|
|
1131
|
+
let origSrc = sourceEl.getAttribute('data-orig-src');
|
|
1132
|
+
|
|
1133
|
+
// Store current src as data-orig-src if not already set
|
|
1134
|
+
if (!origSrc) {
|
|
1135
|
+
origSrc = currentSrc;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// Store info for re-adding with described src
|
|
1139
|
+
sourcesToUpdate.push({
|
|
1140
|
+
src: descSrcAttr, // Use described version
|
|
1141
|
+
type: type,
|
|
1142
|
+
origSrc: origSrc,
|
|
1143
|
+
descSrc: descSrcAttr
|
|
1144
|
+
});
|
|
1145
|
+
} else {
|
|
1146
|
+
// Source element without data-desc-src - keep as-is
|
|
1147
|
+
const type = sourceEl.getAttribute('type');
|
|
1148
|
+
const src = sourceEl.getAttribute('src');
|
|
1149
|
+
sourcesToUpdate.push({
|
|
1150
|
+
src: src,
|
|
1151
|
+
type: type,
|
|
1152
|
+
origSrc: null,
|
|
1153
|
+
descSrc: null
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
// Remove all source elements
|
|
1159
|
+
allSourceElements.forEach(sourceEl => {
|
|
1160
|
+
sourceEl.remove();
|
|
1161
|
+
});
|
|
1162
|
+
|
|
1163
|
+
// Re-add them with updated src attributes (described versions)
|
|
1164
|
+
sourcesToUpdate.forEach(sourceInfo => {
|
|
1165
|
+
const newSource = document.createElement('source');
|
|
1166
|
+
newSource.setAttribute('src', sourceInfo.src);
|
|
1167
|
+
if (sourceInfo.type) {
|
|
1168
|
+
newSource.setAttribute('type', sourceInfo.type);
|
|
1169
|
+
}
|
|
1170
|
+
if (sourceInfo.origSrc) {
|
|
1171
|
+
newSource.setAttribute('data-orig-src', sourceInfo.origSrc);
|
|
1172
|
+
}
|
|
1173
|
+
if (sourceInfo.descSrc) {
|
|
1174
|
+
newSource.setAttribute('data-desc-src', sourceInfo.descSrc);
|
|
1175
|
+
}
|
|
1176
|
+
this.element.appendChild(newSource);
|
|
1177
|
+
});
|
|
1178
|
+
|
|
1179
|
+
// Force reload by calling load() on the element
|
|
1180
|
+
// This should pick up the new src attributes from the re-added source elements
|
|
1181
|
+
// and also reload the track elements
|
|
1182
|
+
this.element.load();
|
|
1183
|
+
|
|
1184
|
+
// Wait for new source to load
|
|
1185
|
+
await new Promise((resolve) => {
|
|
1186
|
+
const onLoadedMetadata = () => {
|
|
1187
|
+
this.element.removeEventListener('loadedmetadata', onLoadedMetadata);
|
|
1188
|
+
resolve();
|
|
1189
|
+
};
|
|
1190
|
+
this.element.addEventListener('loadedmetadata', onLoadedMetadata);
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
// Wait a bit more for tracks to be recognized and loaded after video metadata loads
|
|
1194
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
1195
|
+
|
|
1196
|
+
// Hide poster if video hasn't started yet (poster should hide when we seek or play)
|
|
1197
|
+
if (this.element.tagName === 'VIDEO' && currentTime === 0 && !wasPlaying) {
|
|
1198
|
+
// Force poster to hide by doing a minimal seek or loading first frame
|
|
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
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// Restore playback position
|
|
1211
|
+
this.seek(currentTime);
|
|
1212
|
+
|
|
1213
|
+
if (wasPlaying) {
|
|
1214
|
+
this.play();
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// Update state and emit event
|
|
1218
|
+
this.state.audioDescriptionEnabled = true;
|
|
1219
|
+
this.emit('audiodescriptionenabled');
|
|
1220
|
+
} else {
|
|
1221
|
+
// Fallback to updating element src directly
|
|
1222
|
+
// Swap caption tracks to described versions BEFORE loading
|
|
1223
|
+
if (this.audioDescriptionCaptionTracks.length > 0) {
|
|
1224
|
+
// Swap tracks: validate explicit tracks, but try auto-detected tracks without validation
|
|
1225
|
+
const validationPromises = this.audioDescriptionCaptionTracks.map(async (trackInfo) => {
|
|
1226
|
+
if (trackInfo.trackElement && trackInfo.describedSrc) {
|
|
1227
|
+
// Only validate explicitly defined tracks
|
|
1228
|
+
// Auto-detected tracks are used without validation (no 404s)
|
|
1229
|
+
if (trackInfo.explicit === true) {
|
|
1230
|
+
try {
|
|
1231
|
+
const exists = await this.validateTrackExists(trackInfo.describedSrc);
|
|
1232
|
+
return { trackInfo, exists };
|
|
1233
|
+
} catch (error) {
|
|
1234
|
+
return { trackInfo, exists: false };
|
|
1235
|
+
}
|
|
1236
|
+
} else {
|
|
1237
|
+
// This shouldn't happen since auto-detection is disabled
|
|
1238
|
+
return { trackInfo, exists: false };
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
return { trackInfo, exists: false };
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
const validationResults = await Promise.all(validationPromises);
|
|
1245
|
+
const tracksToSwap = validationResults.filter(result => result.exists);
|
|
1246
|
+
|
|
1247
|
+
if (tracksToSwap.length > 0) {
|
|
1248
|
+
// Store original track modes before removing tracks
|
|
1249
|
+
const trackModes = new Map();
|
|
1250
|
+
tracksToSwap.forEach(({ trackInfo }) => {
|
|
1251
|
+
const textTrack = trackInfo.trackElement.track;
|
|
1252
|
+
if (textTrack) {
|
|
1253
|
+
trackModes.set(trackInfo, {
|
|
1254
|
+
wasShowing: textTrack.mode === 'showing',
|
|
1255
|
+
wasHidden: textTrack.mode === 'hidden'
|
|
1256
|
+
});
|
|
1257
|
+
} else {
|
|
1258
|
+
trackModes.set(trackInfo, {
|
|
1259
|
+
wasShowing: false,
|
|
1260
|
+
wasHidden: false
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
});
|
|
1264
|
+
|
|
1265
|
+
// Store all track information before removing
|
|
1266
|
+
const tracksToReadd = tracksToSwap.map(({ trackInfo }) => {
|
|
1267
|
+
const oldSrc = trackInfo.trackElement.getAttribute('src');
|
|
1268
|
+
const parent = trackInfo.trackElement.parentNode;
|
|
1269
|
+
const nextSibling = trackInfo.trackElement.nextSibling;
|
|
1270
|
+
|
|
1271
|
+
// Store all attributes from the old track
|
|
1272
|
+
const attributes = {};
|
|
1273
|
+
Array.from(trackInfo.trackElement.attributes).forEach(attr => {
|
|
1274
|
+
attributes[attr.name] = attr.value;
|
|
1275
|
+
});
|
|
1276
|
+
|
|
1277
|
+
return {
|
|
1278
|
+
trackInfo,
|
|
1279
|
+
oldSrc,
|
|
1280
|
+
parent,
|
|
1281
|
+
nextSibling,
|
|
1282
|
+
attributes
|
|
1283
|
+
};
|
|
1284
|
+
});
|
|
1285
|
+
|
|
1286
|
+
// Remove ALL old tracks first to force browser to clear TextTrack objects
|
|
1287
|
+
tracksToReadd.forEach(({ trackInfo }) => {
|
|
1288
|
+
trackInfo.trackElement.remove();
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1291
|
+
// Force browser to process the removal by calling load()
|
|
1292
|
+
this.element.load();
|
|
1293
|
+
|
|
1294
|
+
// Wait for browser to process the removal, then add new tracks
|
|
1295
|
+
setTimeout(() => {
|
|
1296
|
+
tracksToReadd.forEach(({ trackInfo, oldSrc, parent, nextSibling, attributes }) => {
|
|
1297
|
+
swappedTracksForTranscript.push(trackInfo);
|
|
1298
|
+
|
|
1299
|
+
// Create a completely new track element (not a clone) to force browser to create new TextTrack
|
|
1300
|
+
const newTrackElement = document.createElement('track');
|
|
1301
|
+
newTrackElement.setAttribute('src', trackInfo.describedSrc);
|
|
1302
|
+
|
|
1303
|
+
// Copy all attributes except src and data-desc-src
|
|
1304
|
+
Object.keys(attributes).forEach(attrName => {
|
|
1305
|
+
if (attrName !== 'src' && attrName !== 'data-desc-src') {
|
|
1306
|
+
newTrackElement.setAttribute(attrName, attributes[attrName]);
|
|
1307
|
+
}
|
|
1308
|
+
});
|
|
1309
|
+
|
|
1310
|
+
// Insert new track element
|
|
1311
|
+
if (nextSibling && nextSibling.parentNode) {
|
|
1312
|
+
parent.insertBefore(newTrackElement, nextSibling);
|
|
1313
|
+
} else {
|
|
1314
|
+
parent.appendChild(newTrackElement);
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// Update reference to the new track element
|
|
1318
|
+
trackInfo.trackElement = newTrackElement;
|
|
1319
|
+
});
|
|
1320
|
+
|
|
1321
|
+
// After all new tracks are added, force browser to reload media element again
|
|
1322
|
+
this.element.load();
|
|
1323
|
+
|
|
1324
|
+
// Wait for loadedmetadata event before accessing new TextTrack objects
|
|
1325
|
+
const setupNewTracks = () => {
|
|
1326
|
+
// Wait a bit more for browser to fully process the new track elements
|
|
1327
|
+
setTimeout(() => {
|
|
1328
|
+
swappedTracksForTranscript.forEach((trackInfo) => {
|
|
1329
|
+
const trackElement = trackInfo.trackElement;
|
|
1330
|
+
const newTextTrack = trackElement.track;
|
|
1331
|
+
|
|
1332
|
+
if (newTextTrack) {
|
|
1333
|
+
// Get original mode from stored map
|
|
1334
|
+
const modeInfo = trackModes.get(trackInfo) || { wasShowing: false, wasHidden: false };
|
|
1335
|
+
|
|
1336
|
+
// Set mode to load the new track
|
|
1337
|
+
newTextTrack.mode = 'hidden'; // Use hidden to load cues without showing
|
|
1338
|
+
|
|
1339
|
+
// Restore original mode after track loads
|
|
1340
|
+
const restoreMode = () => {
|
|
1341
|
+
if (modeInfo.wasShowing) {
|
|
1342
|
+
// Set to hidden - CaptionManager will set it to showing when it enables
|
|
1343
|
+
newTextTrack.mode = 'hidden';
|
|
1344
|
+
} else if (modeInfo.wasHidden) {
|
|
1345
|
+
newTextTrack.mode = 'hidden';
|
|
1346
|
+
} else {
|
|
1347
|
+
newTextTrack.mode = 'disabled';
|
|
1348
|
+
}
|
|
1349
|
+
};
|
|
1350
|
+
|
|
1351
|
+
// Wait for track to load
|
|
1352
|
+
if (newTextTrack.readyState >= 2) { // LOADED
|
|
1353
|
+
restoreMode();
|
|
1354
|
+
} else {
|
|
1355
|
+
newTextTrack.addEventListener('load', restoreMode, { once: true });
|
|
1356
|
+
newTextTrack.addEventListener('error', restoreMode, { once: true });
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
});
|
|
1360
|
+
}, 300); // Additional wait for browser to process track elements
|
|
1361
|
+
};
|
|
1362
|
+
|
|
1363
|
+
// Wait for loadedmetadata event which fires when browser processes track elements
|
|
1364
|
+
if (this.element.readyState >= 1) { // HAVE_METADATA
|
|
1365
|
+
// Already loaded, wait a bit and setup
|
|
1366
|
+
setTimeout(setupNewTracks, 200);
|
|
1367
|
+
} else {
|
|
1368
|
+
this.element.addEventListener('loadedmetadata', setupNewTracks, { once: true });
|
|
1369
|
+
// Fallback timeout
|
|
1370
|
+
setTimeout(setupNewTracks, 2000);
|
|
1371
|
+
}
|
|
1372
|
+
}, 100); // Wait 100ms after first load() before adding new tracks
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
// Check if we have source elements with data-desc-src (fallback method)
|
|
1377
|
+
const fallbackSourceElements = Array.from(this.element.querySelectorAll('source'));
|
|
1378
|
+
const hasSourceElementsWithDesc = fallbackSourceElements.some(el => el.getAttribute('data-desc-src'));
|
|
1379
|
+
|
|
1380
|
+
if (hasSourceElementsWithDesc) {
|
|
1381
|
+
const fallbackSourcesToUpdate = [];
|
|
1382
|
+
|
|
1383
|
+
fallbackSourceElements.forEach((sourceEl) => {
|
|
1384
|
+
const descSrcAttr = sourceEl.getAttribute('data-desc-src');
|
|
1385
|
+
const currentSrc = sourceEl.getAttribute('src');
|
|
1386
|
+
|
|
1387
|
+
if (descSrcAttr) {
|
|
1388
|
+
const type = sourceEl.getAttribute('type');
|
|
1389
|
+
let origSrc = sourceEl.getAttribute('data-orig-src');
|
|
1390
|
+
|
|
1391
|
+
if (!origSrc) {
|
|
1392
|
+
origSrc = currentSrc;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
fallbackSourcesToUpdate.push({
|
|
1396
|
+
src: descSrcAttr,
|
|
1397
|
+
type: type,
|
|
1398
|
+
origSrc: origSrc,
|
|
1399
|
+
descSrc: descSrcAttr
|
|
1400
|
+
});
|
|
1401
|
+
} else {
|
|
1402
|
+
const type = sourceEl.getAttribute('type');
|
|
1403
|
+
const src = sourceEl.getAttribute('src');
|
|
1404
|
+
fallbackSourcesToUpdate.push({
|
|
1405
|
+
src: src,
|
|
1406
|
+
type: type,
|
|
1407
|
+
origSrc: null,
|
|
1408
|
+
descSrc: null
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
});
|
|
1412
|
+
|
|
1413
|
+
// Remove all source elements
|
|
1414
|
+
fallbackSourceElements.forEach(sourceEl => {
|
|
1415
|
+
sourceEl.remove();
|
|
1416
|
+
});
|
|
1417
|
+
|
|
1418
|
+
// Re-add them with updated src attributes
|
|
1419
|
+
fallbackSourcesToUpdate.forEach(sourceInfo => {
|
|
1420
|
+
const newSource = document.createElement('source');
|
|
1421
|
+
newSource.setAttribute('src', sourceInfo.src);
|
|
1422
|
+
if (sourceInfo.type) {
|
|
1423
|
+
newSource.setAttribute('type', sourceInfo.type);
|
|
1424
|
+
}
|
|
1425
|
+
if (sourceInfo.origSrc) {
|
|
1426
|
+
newSource.setAttribute('data-orig-src', sourceInfo.origSrc);
|
|
1427
|
+
}
|
|
1428
|
+
if (sourceInfo.descSrc) {
|
|
1429
|
+
newSource.setAttribute('data-desc-src', sourceInfo.descSrc);
|
|
1430
|
+
}
|
|
1431
|
+
this.element.appendChild(newSource);
|
|
1432
|
+
});
|
|
1433
|
+
|
|
1434
|
+
// Force reload
|
|
1435
|
+
this.element.load();
|
|
1436
|
+
} else {
|
|
1437
|
+
// Fallback to updating element src directly (for videos without source elements)
|
|
1438
|
+
this.element.src = this.audioDescriptionSrc;
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
819
1441
|
|
|
820
1442
|
// Wait for new source to load
|
|
821
1443
|
await new Promise((resolve) => {
|
|
@@ -826,6 +1448,20 @@ export class Player extends EventEmitter {
|
|
|
826
1448
|
this.element.addEventListener('loadedmetadata', onLoadedMetadata);
|
|
827
1449
|
});
|
|
828
1450
|
|
|
1451
|
+
// Hide poster if video hasn't started yet (poster should hide when we seek or play)
|
|
1452
|
+
if (this.element.tagName === 'VIDEO' && currentTime === 0 && !wasPlaying) {
|
|
1453
|
+
// Force poster to hide by doing a minimal seek or loading first frame
|
|
1454
|
+
// Setting readyState check or seeking to 0.001 seconds will hide the poster
|
|
1455
|
+
if (this.element.readyState >= 1) { // HAVE_METADATA
|
|
1456
|
+
// Seek to a tiny fraction to trigger poster hiding without actually moving
|
|
1457
|
+
this.element.currentTime = 0.001;
|
|
1458
|
+
// Then seek back to 0 after a brief moment to ensure poster stays hidden
|
|
1459
|
+
setTimeout(() => {
|
|
1460
|
+
this.element.currentTime = 0;
|
|
1461
|
+
}, 10);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
|
|
829
1465
|
// Restore playback position
|
|
830
1466
|
this.seek(currentTime);
|
|
831
1467
|
|
|
@@ -833,6 +1469,252 @@ export class Player extends EventEmitter {
|
|
|
833
1469
|
this.play();
|
|
834
1470
|
}
|
|
835
1471
|
|
|
1472
|
+
// Reload CaptionManager tracks if tracks were swapped (so it has fresh references)
|
|
1473
|
+
if (swappedTracksForTranscript.length > 0 && this.captionManager) {
|
|
1474
|
+
// Store if captions were enabled and which track
|
|
1475
|
+
const wasCaptionsEnabled = this.state.captionsEnabled;
|
|
1476
|
+
let currentTrackInfo = null;
|
|
1477
|
+
if (this.captionManager.currentTrack) {
|
|
1478
|
+
const currentTrackIndex = this.captionManager.tracks.findIndex(t => t.track === this.captionManager.currentTrack.track);
|
|
1479
|
+
if (currentTrackIndex >= 0) {
|
|
1480
|
+
currentTrackInfo = {
|
|
1481
|
+
language: this.captionManager.tracks[currentTrackIndex].language,
|
|
1482
|
+
kind: this.captionManager.tracks[currentTrackIndex].kind
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
// Wait a bit for new tracks to be available, then reload
|
|
1488
|
+
setTimeout(() => {
|
|
1489
|
+
// Reload tracks to get fresh references to new TextTrack objects
|
|
1490
|
+
this.captionManager.tracks = [];
|
|
1491
|
+
this.captionManager.loadTracks();
|
|
1492
|
+
|
|
1493
|
+
// Re-enable captions if they were enabled before
|
|
1494
|
+
if (wasCaptionsEnabled && currentTrackInfo && this.captionManager.tracks.length > 0) {
|
|
1495
|
+
// Find the track by language and kind to match the swapped track
|
|
1496
|
+
const matchingTrackIndex = this.captionManager.tracks.findIndex(t =>
|
|
1497
|
+
t.language === currentTrackInfo.language && t.kind === currentTrackInfo.kind
|
|
1498
|
+
);
|
|
1499
|
+
|
|
1500
|
+
if (matchingTrackIndex >= 0) {
|
|
1501
|
+
this.captionManager.enable(matchingTrackIndex);
|
|
1502
|
+
} else if (this.captionManager.tracks.length > 0) {
|
|
1503
|
+
// Fallback: enable first track
|
|
1504
|
+
this.captionManager.enable(0);
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
}, 600); // Wait for tracks to be processed
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
// Reload transcript if visible (after video metadata loaded, tracks should be available)
|
|
1511
|
+
// Reload regardless of whether caption tracks were swapped, in case tracks changed
|
|
1512
|
+
if (this.transcriptManager && this.transcriptManager.isVisible) {
|
|
1513
|
+
// Wait for tracks to load after source swap
|
|
1514
|
+
// If tracks were swapped, wait for them to load; otherwise wait a bit for any track changes
|
|
1515
|
+
const swappedTracks = typeof swappedTracksForTranscript !== 'undefined' ? swappedTracksForTranscript : [];
|
|
1516
|
+
|
|
1517
|
+
if (swappedTracks.length > 0) {
|
|
1518
|
+
// Wait for swapped tracks to load their new cues
|
|
1519
|
+
// Since we re-added track elements and called load(), wait for loadedmetadata event
|
|
1520
|
+
// which is when the browser processes track elements
|
|
1521
|
+
const onMetadataLoaded = () => {
|
|
1522
|
+
// Get fresh track references from the video element's textTracks collection
|
|
1523
|
+
// This ensures we get the actual textTrack objects that the browser created
|
|
1524
|
+
const allTextTracks = Array.from(this.element.textTracks);
|
|
1525
|
+
|
|
1526
|
+
// Find the tracks that match our swapped tracks by language and kind
|
|
1527
|
+
// Match by checking the track element's src attribute
|
|
1528
|
+
const freshTracks = swappedTracks.map((trackInfo) => {
|
|
1529
|
+
const trackEl = trackInfo.trackElement;
|
|
1530
|
+
const expectedSrc = trackEl.getAttribute('src');
|
|
1531
|
+
const srclang = trackEl.getAttribute('srclang');
|
|
1532
|
+
const kind = trackEl.getAttribute('kind');
|
|
1533
|
+
|
|
1534
|
+
// Find matching track in textTracks collection
|
|
1535
|
+
// First try to match by the track element reference
|
|
1536
|
+
let foundTrack = allTextTracks.find(track => trackEl.track === track);
|
|
1537
|
+
|
|
1538
|
+
// If not found, try matching by language and kind, but verify src
|
|
1539
|
+
if (!foundTrack) {
|
|
1540
|
+
foundTrack = allTextTracks.find(track => {
|
|
1541
|
+
if (track.language === srclang &&
|
|
1542
|
+
(track.kind === kind || (kind === 'captions' && track.kind === 'subtitles'))) {
|
|
1543
|
+
// Verify the src matches
|
|
1544
|
+
const trackElementForTrack = Array.from(this.element.querySelectorAll('track')).find(
|
|
1545
|
+
el => el.track === track
|
|
1546
|
+
);
|
|
1547
|
+
if (trackElementForTrack) {
|
|
1548
|
+
const actualSrc = trackElementForTrack.getAttribute('src');
|
|
1549
|
+
if (actualSrc === expectedSrc) {
|
|
1550
|
+
return true;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
return false;
|
|
1555
|
+
});
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
// Verify the track element's src matches what we expect
|
|
1559
|
+
if (foundTrack) {
|
|
1560
|
+
const trackElement = Array.from(this.element.querySelectorAll('track')).find(
|
|
1561
|
+
el => el.track === foundTrack
|
|
1562
|
+
);
|
|
1563
|
+
if (trackElement && trackElement.getAttribute('src') !== expectedSrc) {
|
|
1564
|
+
return null;
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
return foundTrack;
|
|
1569
|
+
}).filter(Boolean);
|
|
1570
|
+
|
|
1571
|
+
if (freshTracks.length === 0) {
|
|
1572
|
+
// Fallback: just reload after delay - transcript manager will find tracks itself
|
|
1573
|
+
setTimeout(() => {
|
|
1574
|
+
if (this.transcriptManager && this.transcriptManager.loadTranscriptData) {
|
|
1575
|
+
this.transcriptManager.loadTranscriptData();
|
|
1576
|
+
}
|
|
1577
|
+
}, 1000);
|
|
1578
|
+
return;
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
// Ensure tracks are in hidden mode to load cues for transcript
|
|
1582
|
+
freshTracks.forEach(track => {
|
|
1583
|
+
if (track.mode === 'disabled') {
|
|
1584
|
+
track.mode = 'hidden';
|
|
1585
|
+
}
|
|
1586
|
+
});
|
|
1587
|
+
|
|
1588
|
+
let loadedCount = 0;
|
|
1589
|
+
const checkLoaded = () => {
|
|
1590
|
+
loadedCount++;
|
|
1591
|
+
if (loadedCount >= freshTracks.length) {
|
|
1592
|
+
// Give a bit more time for cues to be fully parsed
|
|
1593
|
+
// Also ensure we're getting the latest TextTrack references
|
|
1594
|
+
setTimeout(() => {
|
|
1595
|
+
if (this.transcriptManager && this.transcriptManager.loadTranscriptData) {
|
|
1596
|
+
// Force transcript manager to get fresh track references
|
|
1597
|
+
// Clear any cached track references by forcing a fresh read
|
|
1598
|
+
// The transcript manager will find tracks from this.element.textTracks
|
|
1599
|
+
// which should now have the new TextTrack objects with the described captions
|
|
1600
|
+
|
|
1601
|
+
// Verify the tracks have the correct src before reloading transcript
|
|
1602
|
+
const allTextTracks = Array.from(this.element.textTracks);
|
|
1603
|
+
const swappedTrackSrcs = swappedTracks.map(t => t.describedSrc);
|
|
1604
|
+
const hasCorrectTracks = freshTracks.some(track => {
|
|
1605
|
+
const trackEl = Array.from(this.element.querySelectorAll('track')).find(
|
|
1606
|
+
el => el.track === track
|
|
1607
|
+
);
|
|
1608
|
+
return trackEl && swappedTrackSrcs.includes(trackEl.getAttribute('src'));
|
|
1609
|
+
});
|
|
1610
|
+
|
|
1611
|
+
if (hasCorrectTracks || freshTracks.length > 0) {
|
|
1612
|
+
this.transcriptManager.loadTranscriptData();
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
}, 800); // Increased wait time to ensure cues are fully loaded
|
|
1616
|
+
}
|
|
1617
|
+
};
|
|
1618
|
+
|
|
1619
|
+
freshTracks.forEach(track => {
|
|
1620
|
+
// Ensure track is in hidden mode to load cues (required for transcript)
|
|
1621
|
+
if (track.mode === 'disabled') {
|
|
1622
|
+
track.mode = 'hidden';
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
// Check if track has cues loaded
|
|
1626
|
+
// Verify the track element's src matches the expected described src
|
|
1627
|
+
const trackElementForTrack = Array.from(this.element.querySelectorAll('track')).find(
|
|
1628
|
+
el => el.track === track
|
|
1629
|
+
);
|
|
1630
|
+
const actualSrc = trackElementForTrack ? trackElementForTrack.getAttribute('src') : null;
|
|
1631
|
+
|
|
1632
|
+
// Find the expected src from swappedTracks
|
|
1633
|
+
const expectedTrackInfo = swappedTracks.find(t => {
|
|
1634
|
+
const tEl = t.trackElement;
|
|
1635
|
+
return tEl && (tEl.track === track ||
|
|
1636
|
+
(tEl.getAttribute('srclang') === track.language &&
|
|
1637
|
+
tEl.getAttribute('kind') === track.kind));
|
|
1638
|
+
});
|
|
1639
|
+
const expectedSrc = expectedTrackInfo ? expectedTrackInfo.describedSrc : null;
|
|
1640
|
+
|
|
1641
|
+
// Only proceed if the src matches (or we can't verify)
|
|
1642
|
+
if (expectedSrc && actualSrc && actualSrc !== expectedSrc) {
|
|
1643
|
+
// Wrong track, skip it
|
|
1644
|
+
checkLoaded(); // Count it as loaded to not block
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
if (track.readyState >= 2 && track.cues && track.cues.length > 0) { // LOADED with cues
|
|
1649
|
+
// Track already loaded with cues
|
|
1650
|
+
checkLoaded();
|
|
1651
|
+
} else {
|
|
1652
|
+
// Force track to load by setting mode
|
|
1653
|
+
if (track.mode === 'disabled') {
|
|
1654
|
+
track.mode = 'hidden';
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
// Wait for track to load
|
|
1658
|
+
const onTrackLoad = () => {
|
|
1659
|
+
// Wait a bit for cues to be fully parsed
|
|
1660
|
+
setTimeout(checkLoaded, 300);
|
|
1661
|
+
};
|
|
1662
|
+
|
|
1663
|
+
if (track.readyState >= 2) {
|
|
1664
|
+
// Already loaded, but might not have cues yet
|
|
1665
|
+
// Wait a bit and check again
|
|
1666
|
+
setTimeout(() => {
|
|
1667
|
+
if (track.cues && track.cues.length > 0) {
|
|
1668
|
+
checkLoaded();
|
|
1669
|
+
} else {
|
|
1670
|
+
// Still no cues, wait for load event
|
|
1671
|
+
track.addEventListener('load', onTrackLoad, { once: true });
|
|
1672
|
+
}
|
|
1673
|
+
}, 100);
|
|
1674
|
+
} else {
|
|
1675
|
+
track.addEventListener('load', onTrackLoad, { once: true });
|
|
1676
|
+
track.addEventListener('error', () => {
|
|
1677
|
+
// Even on error, try to reload transcript
|
|
1678
|
+
checkLoaded();
|
|
1679
|
+
}, { once: true });
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
});
|
|
1683
|
+
};
|
|
1684
|
+
|
|
1685
|
+
// Wait for loadedmetadata event which fires when browser processes track elements
|
|
1686
|
+
// Also wait for the tracks to be fully processed after the second load()
|
|
1687
|
+
const waitForTracks = () => {
|
|
1688
|
+
// Wait a bit more to ensure new TextTrack objects are created
|
|
1689
|
+
setTimeout(() => {
|
|
1690
|
+
if (this.element.readyState >= 1) { // HAVE_METADATA
|
|
1691
|
+
onMetadataLoaded();
|
|
1692
|
+
} else {
|
|
1693
|
+
this.element.addEventListener('loadedmetadata', onMetadataLoaded, { once: true });
|
|
1694
|
+
// Fallback timeout
|
|
1695
|
+
setTimeout(onMetadataLoaded, 2000);
|
|
1696
|
+
}
|
|
1697
|
+
}, 500); // Wait 500ms after second load() for tracks to be processed
|
|
1698
|
+
};
|
|
1699
|
+
|
|
1700
|
+
waitForTracks();
|
|
1701
|
+
|
|
1702
|
+
// Fallback timeout - longer to ensure tracks are loaded
|
|
1703
|
+
setTimeout(() => {
|
|
1704
|
+
if (this.transcriptManager && this.transcriptManager.loadTranscriptData) {
|
|
1705
|
+
this.transcriptManager.loadTranscriptData();
|
|
1706
|
+
}
|
|
1707
|
+
}, 5000);
|
|
1708
|
+
} else {
|
|
1709
|
+
// No tracks swapped, just wait a bit and reload
|
|
1710
|
+
setTimeout(() => {
|
|
1711
|
+
if (this.transcriptManager && this.transcriptManager.loadTranscriptData) {
|
|
1712
|
+
this.transcriptManager.loadTranscriptData();
|
|
1713
|
+
}
|
|
1714
|
+
}, 800);
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
|
|
836
1718
|
this.state.audioDescriptionEnabled = true;
|
|
837
1719
|
this.emit('audiodescriptionenabled');
|
|
838
1720
|
}
|
|
@@ -846,8 +1728,78 @@ export class Player extends EventEmitter {
|
|
|
846
1728
|
const currentTime = this.state.currentTime;
|
|
847
1729
|
const wasPlaying = this.state.playing;
|
|
848
1730
|
|
|
849
|
-
//
|
|
850
|
-
this.
|
|
1731
|
+
// Swap caption/chapter tracks back to original versions BEFORE loading
|
|
1732
|
+
if (this.audioDescriptionCaptionTracks.length > 0) {
|
|
1733
|
+
this.audioDescriptionCaptionTracks.forEach(trackInfo => {
|
|
1734
|
+
if (trackInfo.trackElement && trackInfo.originalTrackSrc) {
|
|
1735
|
+
trackInfo.trackElement.setAttribute('src', trackInfo.originalTrackSrc);
|
|
1736
|
+
}
|
|
1737
|
+
});
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
// Swap source elements back to original versions
|
|
1741
|
+
// Check if we have source elements with data-orig-src
|
|
1742
|
+
const allSourceElements = Array.from(this.element.querySelectorAll('source'));
|
|
1743
|
+
const hasSourceElementsToSwap = allSourceElements.some(el => el.getAttribute('data-orig-src'));
|
|
1744
|
+
|
|
1745
|
+
if (hasSourceElementsToSwap) {
|
|
1746
|
+
const sourcesToRestore = [];
|
|
1747
|
+
|
|
1748
|
+
allSourceElements.forEach((sourceEl) => {
|
|
1749
|
+
const origSrcAttr = sourceEl.getAttribute('data-orig-src');
|
|
1750
|
+
const descSrcAttr = sourceEl.getAttribute('data-desc-src');
|
|
1751
|
+
|
|
1752
|
+
if (origSrcAttr) {
|
|
1753
|
+
// Swap back to original src
|
|
1754
|
+
const type = sourceEl.getAttribute('type');
|
|
1755
|
+
sourcesToRestore.push({
|
|
1756
|
+
src: origSrcAttr, // Use original version
|
|
1757
|
+
type: type,
|
|
1758
|
+
origSrc: origSrcAttr,
|
|
1759
|
+
descSrc: descSrcAttr // Keep data-desc-src for future swaps
|
|
1760
|
+
});
|
|
1761
|
+
} else {
|
|
1762
|
+
// Keep as-is (no data-orig-src means it wasn't swapped)
|
|
1763
|
+
const type = sourceEl.getAttribute('type');
|
|
1764
|
+
const src = sourceEl.getAttribute('src');
|
|
1765
|
+
sourcesToRestore.push({
|
|
1766
|
+
src: src,
|
|
1767
|
+
type: type,
|
|
1768
|
+
origSrc: null,
|
|
1769
|
+
descSrc: descSrcAttr
|
|
1770
|
+
});
|
|
1771
|
+
}
|
|
1772
|
+
});
|
|
1773
|
+
|
|
1774
|
+
// Remove all source elements
|
|
1775
|
+
allSourceElements.forEach(sourceEl => {
|
|
1776
|
+
sourceEl.remove();
|
|
1777
|
+
});
|
|
1778
|
+
|
|
1779
|
+
// Re-add them with original src attributes
|
|
1780
|
+
sourcesToRestore.forEach(sourceInfo => {
|
|
1781
|
+
const newSource = document.createElement('source');
|
|
1782
|
+
newSource.setAttribute('src', sourceInfo.src);
|
|
1783
|
+
if (sourceInfo.type) {
|
|
1784
|
+
newSource.setAttribute('type', sourceInfo.type);
|
|
1785
|
+
}
|
|
1786
|
+
if (sourceInfo.origSrc) {
|
|
1787
|
+
newSource.setAttribute('data-orig-src', sourceInfo.origSrc);
|
|
1788
|
+
}
|
|
1789
|
+
if (sourceInfo.descSrc) {
|
|
1790
|
+
newSource.setAttribute('data-desc-src', sourceInfo.descSrc);
|
|
1791
|
+
}
|
|
1792
|
+
this.element.appendChild(newSource);
|
|
1793
|
+
});
|
|
1794
|
+
|
|
1795
|
+
// Force reload
|
|
1796
|
+
this.element.load();
|
|
1797
|
+
} else {
|
|
1798
|
+
// Fallback to updating element src directly (for videos without source elements)
|
|
1799
|
+
const originalSrcToUse = this.originalAudioDescriptionSource || this.originalSrc;
|
|
1800
|
+
this.element.src = originalSrcToUse;
|
|
1801
|
+
this.element.load();
|
|
1802
|
+
}
|
|
851
1803
|
|
|
852
1804
|
// Wait for new source to load
|
|
853
1805
|
await new Promise((resolve) => {
|
|
@@ -865,6 +1817,17 @@ export class Player extends EventEmitter {
|
|
|
865
1817
|
this.play();
|
|
866
1818
|
}
|
|
867
1819
|
|
|
1820
|
+
// Reload transcript if visible (after video metadata loaded, tracks should be available)
|
|
1821
|
+
// Reload regardless of whether caption tracks were swapped, in case tracks changed
|
|
1822
|
+
if (this.transcriptManager && this.transcriptManager.isVisible) {
|
|
1823
|
+
// Wait for tracks to load after source swap
|
|
1824
|
+
setTimeout(() => {
|
|
1825
|
+
if (this.transcriptManager && this.transcriptManager.loadTranscriptData) {
|
|
1826
|
+
this.transcriptManager.loadTranscriptData();
|
|
1827
|
+
}
|
|
1828
|
+
}, 500);
|
|
1829
|
+
}
|
|
1830
|
+
|
|
868
1831
|
this.state.audioDescriptionEnabled = false;
|
|
869
1832
|
this.emit('audiodescriptiondisabled');
|
|
870
1833
|
}
|
|
@@ -874,7 +1837,23 @@ export class Player extends EventEmitter {
|
|
|
874
1837
|
const textTracks = Array.from(this.element.textTracks || []);
|
|
875
1838
|
const descriptionTrack = textTracks.find(track => track.kind === 'descriptions');
|
|
876
1839
|
|
|
877
|
-
if (
|
|
1840
|
+
// Check if we have audio-described video source (either from options or source elements with data-desc-src)
|
|
1841
|
+
const hasAudioDescriptionSrc = this.audioDescriptionSrc ||
|
|
1842
|
+
Array.from(this.element.querySelectorAll('source')).some(el => el.getAttribute('data-desc-src'));
|
|
1843
|
+
|
|
1844
|
+
if (descriptionTrack && hasAudioDescriptionSrc) {
|
|
1845
|
+
// We have both: toggle description track AND swap caption tracks/sources
|
|
1846
|
+
if (this.state.audioDescriptionEnabled) {
|
|
1847
|
+
// Disable: toggle description track off and swap captions/sources back
|
|
1848
|
+
descriptionTrack.mode = 'hidden';
|
|
1849
|
+
await this.disableAudioDescription();
|
|
1850
|
+
} else {
|
|
1851
|
+
// Enable: swap caption tracks/sources and toggle description track on
|
|
1852
|
+
await this.enableAudioDescription();
|
|
1853
|
+
descriptionTrack.mode = 'showing';
|
|
1854
|
+
}
|
|
1855
|
+
} else if (descriptionTrack) {
|
|
1856
|
+
// Only description track, no audio-described video source to swap
|
|
878
1857
|
// Toggle description track
|
|
879
1858
|
if (descriptionTrack.mode === 'showing') {
|
|
880
1859
|
descriptionTrack.mode = 'hidden';
|
|
@@ -885,8 +1864,8 @@ export class Player extends EventEmitter {
|
|
|
885
1864
|
this.state.audioDescriptionEnabled = true;
|
|
886
1865
|
this.emit('audiodescriptionenabled');
|
|
887
1866
|
}
|
|
888
|
-
} else if (
|
|
889
|
-
// Use audio-described video source
|
|
1867
|
+
} else if (hasAudioDescriptionSrc) {
|
|
1868
|
+
// Use audio-described video source (no description track)
|
|
890
1869
|
if (this.state.audioDescriptionEnabled) {
|
|
891
1870
|
await this.disableAudioDescription();
|
|
892
1871
|
} else {
|