@grfzhl/vue-hls-player 1.1.22 → 1.1.23
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/README.md +43 -39
- package/dist/VideoPlayer/BasePlayer.vue +273 -303
- package/dist/VideoPlayer/VDefaultVideoPlayer.vue +5 -0
- package/dist/VideoPlayer/index.vue +5 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -207,71 +207,75 @@ At the moment the following attribute are supported:
|
|
|
207
207
|
```
|
|
208
208
|
|
|
209
209
|
### Last release:
|
|
210
|
+
v1.1.23
|
|
211
|
+
- Fix missing property for subtitles in vue definition
|
|
212
|
+
- Clean up code
|
|
213
|
+
- Set mutation observer to body and the video element itself - whatever loads first
|
|
210
214
|
v1.1.22
|
|
211
|
-
- Only show language selection if subtitles or audio options are > 1
|
|
215
|
+
- Only show language selection if subtitles or audio options are > 1
|
|
212
216
|
v1.1.21
|
|
213
|
-
- Added more tolerant processing for .txt transcripts to allow empty lines as narrators
|
|
217
|
+
- Added more tolerant processing for .txt transcripts to allow empty lines as narrators
|
|
214
218
|
v1.1.20
|
|
215
|
-
- Stability and UI style improvements for Language switch
|
|
219
|
+
- Stability and UI style improvements for Language switch
|
|
216
220
|
v1.1.19
|
|
217
|
-
- Switcher supports both single and multi-language HLS sources
|
|
218
|
-
- Automatically syncs subtitle language with selected audio track
|
|
219
|
-
- Works with videos that include or omit subtitles
|
|
220
|
-
- Includes native cuechange event handling for subtitle updates
|
|
221
|
-
- Removed the default media-captions (CC) button from the player
|
|
221
|
+
- Switcher supports both single and multi-language HLS sources
|
|
222
|
+
- Automatically syncs subtitle language with selected audio track
|
|
223
|
+
- Works with videos that include or omit subtitles
|
|
224
|
+
- Includes native cuechange event handling for subtitle updates
|
|
225
|
+
- Removed the default media-captions (CC) button from the player
|
|
222
226
|
v1.1.18
|
|
223
|
-
- Added new slot `between-video-and-transcript` to `BasePlayer.vue`, `VDefaultVideoPlayer.vue` and `index.vue`
|
|
227
|
+
- Added new slot `between-video-and-transcript` to `BasePlayer.vue`, `VDefaultVideoPlayer.vue` and `index.vue`
|
|
224
228
|
to allow injection of custom UI between video and transcript.
|
|
225
|
-
- Introduced a continuous **Frame Pointer Loop** that emits a `pointer-update` event with the current playback time and calculated frame number (30 fps) for real-time frame tracking.
|
|
229
|
+
- Introduced a continuous **Frame Pointer Loop** that emits a `pointer-update` event with the current playback time and calculated frame number (30 fps) for real-time frame tracking.
|
|
226
230
|
v1.1.17
|
|
227
|
-
- Keep query params for transcription when getting from .vtt file to .txt
|
|
231
|
+
- Keep query params for transcription when getting from .vtt file to .txt
|
|
228
232
|
v1.1.16
|
|
229
|
-
- Add the options prop to both index.vue and VpDefaultVideoPlayer.vue to pass fullscreen label settings to the BasePlayer component.
|
|
230
|
-
- Update fullscreen toggle logic: adjust aria-label, add or remove mediaIsFullscreen attribute, and safely access media-tooltip via shadowRoot to ensure proper icon and tooltip state handling.
|
|
233
|
+
- Add the options prop to both index.vue and VpDefaultVideoPlayer.vue to pass fullscreen label settings to the BasePlayer component.
|
|
234
|
+
- Update fullscreen toggle logic: adjust aria-label, add or remove mediaIsFullscreen attribute, and safely access media-tooltip via shadowRoot to ensure proper icon and tooltip state handling.
|
|
231
235
|
v1.1.13 - v1.1.15
|
|
232
|
-
- Update the hls.js package
|
|
233
|
-
- Fixes
|
|
236
|
+
- Update the hls.js package
|
|
237
|
+
- Fixes
|
|
234
238
|
|
|
235
239
|
v1.1.12
|
|
236
|
-
- added component property to make it easier adding headers (like Authorization header into every hls request)
|
|
240
|
+
- added component property to make it easier adding headers (like Authorization header into every hls request)
|
|
237
241
|
|
|
238
242
|
v1.0.14 - v1.1.11
|
|
239
|
-
- Fixes
|
|
240
|
-
- iOS specific improvements
|
|
241
|
-
- New Options to customize the component
|
|
242
|
-
- Fix problem with not updating transcript highlighting
|
|
243
|
-
- Extending overloaded functions for fullscreen mode (WIP)
|
|
243
|
+
- Fixes
|
|
244
|
+
- iOS specific improvements
|
|
245
|
+
- New Options to customize the component
|
|
246
|
+
- Fix problem with not updating transcript highlighting
|
|
247
|
+
- Extending overloaded functions for fullscreen mode (WIP)
|
|
244
248
|
|
|
245
249
|
v1.0.9 - v1.0.14
|
|
246
|
-
- Fixes
|
|
247
|
-
- Small styling improvements
|
|
250
|
+
- Fixes
|
|
251
|
+
- Small styling improvements
|
|
248
252
|
|
|
249
253
|
v1.0.9
|
|
250
|
-
- Fix sizes in fullscreen mode for video
|
|
251
|
-
- Hide transcript block completely when hidden
|
|
254
|
+
- Fix sizes in fullscreen mode for video
|
|
255
|
+
- Hide transcript block completely when hidden
|
|
252
256
|
|
|
253
257
|
v1.0.8
|
|
254
|
-
- Add slots to inject own elements nearby video element
|
|
255
|
-
- Add prop for autoplay video
|
|
258
|
+
- Add slots to inject own elements nearby video element
|
|
259
|
+
- Add prop for autoplay video
|
|
256
260
|
|
|
257
261
|
v1.0.7
|
|
258
|
-
- Add function to handle own logic for fullscreen
|
|
262
|
+
- Add function to handle own logic for fullscreen
|
|
259
263
|
|
|
260
264
|
v1.0.6
|
|
261
|
-
- Small fixes
|
|
262
|
-
- Remove debug log
|
|
265
|
+
- Small fixes
|
|
266
|
+
- Remove debug log
|
|
263
267
|
|
|
264
268
|
v1.0.5
|
|
265
|
-
- Load transcriptions additionally to subtitles
|
|
266
|
-
- Add styled transcription block for better readability
|
|
267
|
-
- Improve interaction and dynamic params
|
|
269
|
+
- Load transcriptions additionally to subtitles
|
|
270
|
+
- Add styled transcription block for better readability
|
|
271
|
+
- Improve interaction and dynamic params
|
|
268
272
|
|
|
269
273
|
v1.0.4
|
|
270
|
-
- Make subtitles dynamic
|
|
271
|
-
- Add new switch to disable the subtitle block
|
|
272
|
-
- Fix some minor issues
|
|
274
|
+
- Make subtitles dynamic
|
|
275
|
+
- Add new switch to disable the subtitle block
|
|
276
|
+
- Fix some minor issues
|
|
273
277
|
|
|
274
278
|
v1.0.3
|
|
275
|
-
- Removed controls in favour of themable overlay by `player.style`.
|
|
276
|
-
- Updated hls library
|
|
277
|
-
- Added styled caption overlays. Added separate container to show all captions.
|
|
279
|
+
- Removed controls in favour of themable overlay by `player.style`.
|
|
280
|
+
- Updated hls library
|
|
281
|
+
- Added styled caption overlays. Added separate container to show all captions.
|
|
@@ -179,6 +179,7 @@ const initialPlayButton = ref(false);
|
|
|
179
179
|
const hideInitialPlayButton = ref(false);
|
|
180
180
|
const transcriptRef = ref(null);
|
|
181
181
|
const previewImageLink = toRef(props, 'previewImageLink');
|
|
182
|
+
const subtitles = toRef(props, 'subtitles');
|
|
182
183
|
const link = toRef(props, 'link');
|
|
183
184
|
let currentTime = 0
|
|
184
185
|
let hls = null
|
|
@@ -232,6 +233,7 @@ async function selectLang(lang) {
|
|
|
232
233
|
currentSubtitleLang.value = lang;
|
|
233
234
|
console.log('[LangSwitch] currentSubtitleLang set to', currentSubtitleLang.value);
|
|
234
235
|
});
|
|
236
|
+
// Attach HLS
|
|
235
237
|
await hls.loadSource(newSource.file_url);
|
|
236
238
|
video.value.muted = true;
|
|
237
239
|
} catch (err) {
|
|
@@ -254,7 +256,6 @@ function updateLangMenuState() {
|
|
|
254
256
|
const langElement = li.querySelector('span[data-lang]')
|
|
255
257
|
const liLang = langElement?.dataset?.lang;
|
|
256
258
|
// const liLang = li.textContent.trim().toLowerCase();
|
|
257
|
-
console.log("-- audio li lang", currentLang.value, liLang)
|
|
258
259
|
li.classList.toggle('active', liLang === currentLang.value.toLowerCase());
|
|
259
260
|
});
|
|
260
261
|
|
|
@@ -272,26 +273,6 @@ function updateLangMenuState() {
|
|
|
272
273
|
|
|
273
274
|
watch(currentSubtitleLang, () => updateLangMenuState());
|
|
274
275
|
|
|
275
|
-
/* function selectLang(lang) {
|
|
276
|
-
currentLang.value = lang;
|
|
277
|
-
showLangMenu.value = false;
|
|
278
|
-
|
|
279
|
-
const newSource = props.multiLangSources.find(s => s.lang === lang);
|
|
280
|
-
if (newSource) {
|
|
281
|
-
hls.stopLoad();
|
|
282
|
-
hls.detachMedia();
|
|
283
|
-
hls.loadSource(newSource.file_url);
|
|
284
|
-
hls.attachMedia(video.value);
|
|
285
|
-
hls.startLoad();
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// Subtitle sync
|
|
289
|
-
const textTracks = video.value.textTracks;
|
|
290
|
-
for (const track of textTracks) {
|
|
291
|
-
track.mode = track.language === lang ? 'showing' : 'disabled';
|
|
292
|
-
}
|
|
293
|
-
} */
|
|
294
|
-
|
|
295
276
|
// --- Frame Pointer Loop ---
|
|
296
277
|
let rafId = null
|
|
297
278
|
const FPS = 30
|
|
@@ -537,7 +518,6 @@ function prepareVideoPlayer(link) {
|
|
|
537
518
|
|
|
538
519
|
// Preparing video player with link: ${link}
|
|
539
520
|
hls = new Hls(hlsConfig);
|
|
540
|
-
|
|
541
521
|
// Attach HLS
|
|
542
522
|
hls.loadSource(link);
|
|
543
523
|
hls.attachMedia(video.value);
|
|
@@ -588,6 +568,7 @@ function prepareVideoPlayer(link) {
|
|
|
588
568
|
});
|
|
589
569
|
}
|
|
590
570
|
|
|
571
|
+
selectLang(props.defaultLang);
|
|
591
572
|
// HLS attached to <video>
|
|
592
573
|
hls.recoverMediaError();
|
|
593
574
|
|
|
@@ -639,284 +620,283 @@ function initVideo() {
|
|
|
639
620
|
* overwrite player.style video fullscreen button
|
|
640
621
|
* to inject own fullscreen logic
|
|
641
622
|
*/
|
|
642
|
-
const observer = new MutationObserver(
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
if (fullscreenButton) {
|
|
652
|
-
fullscreenButton.handleClick = async (event) => {
|
|
653
|
-
event.preventDefault();
|
|
654
|
-
event.stopPropagation();
|
|
655
|
-
event.stopImmediatePropagation();
|
|
656
|
-
startFullscreen();
|
|
657
|
-
}
|
|
658
|
-
// --- Remove default CC button ---
|
|
659
|
-
const ccButton = mediaTheme.shadowRoot.querySelector('media-captions-button');
|
|
660
|
-
if (ccButton) {
|
|
661
|
-
ccButton.remove();
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
// --- Amazon Prime Style Language Switcher ---
|
|
665
|
-
if (controlBar && !controlBar.querySelector('.lang-switcher')
|
|
666
|
-
&& (props.multiLangSources.length > 1 || props.subtitles.length > 1)) {
|
|
667
|
-
const langDiv = document.createElement('div');
|
|
668
|
-
langDiv.className = 'lang-switcher';
|
|
669
|
-
langDiv.innerHTML = `
|
|
670
|
-
<style>
|
|
671
|
-
.lang-switcher {
|
|
672
|
-
position: relative;
|
|
673
|
-
display: flex;
|
|
674
|
-
align-items: center;
|
|
675
|
-
justify-content: center;
|
|
676
|
-
margin-left: var(--media-control-spacing, 6px);
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
.lang-switcher button {
|
|
680
|
-
all: unset;
|
|
681
|
-
display: flex;
|
|
682
|
-
align-items: center;
|
|
683
|
-
justify-content: center;
|
|
684
|
-
cursor: pointer;
|
|
685
|
-
height: var(--media-button-height, 32px);
|
|
686
|
-
border-radius: 25%;
|
|
687
|
-
min-width: var(--media-button-height, 32px);
|
|
688
|
-
padding: var(--media-button-padding, 0 5px);
|
|
689
|
-
background: transparent;
|
|
690
|
-
transition: background 0.15s ease, transform 0.1s ease;
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
.lang-switcher button svg {
|
|
694
|
-
width: var(--media-icon-size, 24px);
|
|
695
|
-
height: var(--media-icon-size, 24px);
|
|
696
|
-
color: var(--media-icon-color, white);
|
|
697
|
-
stroke: currentColor;
|
|
698
|
-
opacity: var(--media-icon-opacity, 0.9);
|
|
699
|
-
pointer-events: none;
|
|
700
|
-
transition: opacity 0.15s ease;
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
.lang-switcher button:hover svg {
|
|
704
|
-
transform: scale(1.1);
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
.lang-switcher button:hover {
|
|
708
|
-
opacity: var(--media-icon-opacity-hover, 1);
|
|
709
|
-
background: var(--media-control-hover-background, rgba(50 50 70 / .7));
|
|
710
|
-
transition: backdrop-filter 0.3s, -webkit-backdrop-filter 0.3s;
|
|
711
|
-
box-shadow: rgba(0, 0, 0, 0.3) 0px 0px 5px;
|
|
712
|
-
backdrop-filter: blur(10px) invert(15%) brightness(80%) opacity(1);
|
|
713
|
-
padding: 5px;
|
|
714
|
-
color: var(--media-text-color, var(--media-primary-color, rgb(238 238 238)));
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
.lang-btn[title]:hover::after {
|
|
718
|
-
content: attr(title);
|
|
719
|
-
position: fixed;
|
|
720
|
-
bottom: 122%;
|
|
721
|
-
right: -100%;
|
|
722
|
-
white-space: nowrap;
|
|
723
|
-
background: var(--media-tooltip-background, rgba(0,0,0,0.2));
|
|
724
|
-
color: var(--media-tooltip-color, #fff);
|
|
725
|
-
border-radius: 4px;
|
|
726
|
-
padding: 4px 15px;
|
|
727
|
-
font-size: var(--media-font-size, 12px);
|
|
728
|
-
box-shadow: 0 2px 8px rgba(0,0,0,0.4);
|
|
729
|
-
line-height: calc(1.2 * var(--base));
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
.lang-menu {
|
|
733
|
-
position: absolute;
|
|
734
|
-
bottom: calc(var(--media-button-height, 32px) * 1.7);
|
|
735
|
-
right: -48px;
|
|
736
|
-
background: var(--media-control-bar-background, rgba(20,20,20,0.4));
|
|
737
|
-
backdrop-filter: blur(12px);
|
|
738
|
-
border-radius: 12px;
|
|
739
|
-
box-shadow: rgba(0, 0, 0, 0.3) 0px 0px 5px;
|
|
740
|
-
padding: 10px;
|
|
741
|
-
min-width: 240px;
|
|
742
|
-
display: none;
|
|
743
|
-
animation: fadeUp 0.15s ease-out;
|
|
744
|
-
color: var(--media-text-color, #fff);
|
|
745
|
-
font-family: var(--media-font-family, system-ui, sans-serif);
|
|
746
|
-
font-size: var(--media-font-size, 13px);
|
|
747
|
-
z-index: 9999;
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
@keyframes fadeUp {
|
|
751
|
-
from { opacity: 0; transform: translateY(8px); }
|
|
752
|
-
to { opacity: 1; transform: translateY(0); }
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
.lang-columns {
|
|
756
|
-
display: flex;
|
|
757
|
-
justify-content: space-between;
|
|
758
|
-
gap: 1rem;
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
.lang-col {
|
|
762
|
-
flex: 1;
|
|
763
|
-
display: flex;
|
|
764
|
-
flex-direction: column;
|
|
765
|
-
margin: 0;
|
|
766
|
-
padding: 0;
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
.lang-col .title {
|
|
770
|
-
line-height: calc(1.2 * var(--base));
|
|
771
|
-
font-weight: 600;
|
|
772
|
-
font-size: calc(var(--media-font-size, 13px) - 1px);
|
|
773
|
-
margin-bottom: 4px;
|
|
774
|
-
border-bottom: 1px solid rgba(255,255,255,0.1);
|
|
775
|
-
padding-bottom: 2px;
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
.lang-col ul {
|
|
779
|
-
list-style: none;
|
|
780
|
-
margin: 0;
|
|
781
|
-
padding: 0;
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
.lang-col li {
|
|
785
|
-
display: flex;
|
|
786
|
-
align-items: center;
|
|
787
|
-
justify-content: space-between;
|
|
788
|
-
cursor: pointer;
|
|
789
|
-
border-radius: 4px;
|
|
790
|
-
padding: 2px 12px;
|
|
791
|
-
transition: background 0.15s ease, color 0.15s ease;
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
.lang-col li span {
|
|
795
|
-
line-height: calc(1.2 * var(--base));
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
.lang-col li:hover {
|
|
799
|
-
background: var(--media-control-hover-background, rgba(255,255,255,0.15));
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
.lang-col li.active {
|
|
803
|
-
font-weight: 500;
|
|
804
|
-
color: var(--media-accent-color, #fff);
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
.lang-col li .icon {
|
|
808
|
-
opacity: 0;
|
|
809
|
-
transition: opacity 0.15s ease, transform 0.15s ease;
|
|
810
|
-
transform: translateX(2px);
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
.lang-col li.active .icon {
|
|
814
|
-
opacity: 1;
|
|
815
|
-
transform: translateX(0);
|
|
816
|
-
}
|
|
817
|
-
</style>
|
|
818
|
-
|
|
819
|
-
<button title="Audio & Subtitles" class="lang-btn">
|
|
820
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 26 26" fill="none" stroke="currentColor" stroke-width="1.5"
|
|
821
|
-
stroke-linecap="round" stroke-linejoin="round">
|
|
822
|
-
<path d="m5 8 6 6"/>
|
|
823
|
-
<path d="m4 14 6-6 2-3"/>
|
|
824
|
-
<path d="M2 5h12"/>
|
|
825
|
-
<path d="M7 2h1"/>
|
|
826
|
-
<path d="m22 22-5-10-5 10"/>
|
|
827
|
-
<path d="M14 18h6"/>
|
|
828
|
-
</svg>
|
|
829
|
-
</button>
|
|
830
|
-
|
|
831
|
-
<div class="lang-menu">
|
|
832
|
-
<div class="lang-columns">
|
|
833
|
-
<ul class="lang-col audio-col"><li class="title">Audio</li></ul>
|
|
834
|
-
<ul class="lang-col sub-col"><li class="title">Subtitles</li></ul>
|
|
835
|
-
</div>
|
|
836
|
-
</div>
|
|
837
|
-
`;
|
|
838
|
-
|
|
839
|
-
const menu = langDiv.querySelector('.lang-menu');
|
|
840
|
-
const audioCol = menu.querySelector('.audio-col');
|
|
841
|
-
const subCol = menu.querySelector('.sub-col');
|
|
842
|
-
const button = langDiv.querySelector('button');
|
|
843
|
-
menu.addEventListener('click', e => e.stopPropagation());
|
|
844
|
-
// Chrome Media Player style check icon
|
|
845
|
-
const renderIcon = () => `
|
|
846
|
-
<svg aria-hidden="true" viewBox="0 1 24 24" width="16" height="16" part="checked-indicator indicator">
|
|
847
|
-
<path fill="currentColor" d="m10 15.17 9.193-9.191 1.414 1.414-10.606 10.606-6.364-6.364 1.414-1.414 4.95 4.95Z"></path>
|
|
848
|
-
</svg>
|
|
849
|
-
`;
|
|
850
|
-
|
|
851
|
-
// Audio options
|
|
852
|
-
props.multiLangSources.forEach(src => {
|
|
853
|
-
const li = document.createElement('li');
|
|
854
|
-
li.innerHTML = `
|
|
855
|
-
<span data-lang="${ src.lang }">${src.label || src.lang.toUpperCase()}</span>
|
|
856
|
-
<span class="icon">${renderIcon()}</span>
|
|
857
|
-
`;
|
|
858
|
-
if (src.lang === currentLang.value) li.classList.add('active');
|
|
859
|
-
li.addEventListener('click', () => {
|
|
860
|
-
audioCol.querySelectorAll('li').forEach(el => el.classList.remove('active'));
|
|
861
|
-
li.classList.add('active');
|
|
862
|
-
selectLang(src.lang);
|
|
863
|
-
menu.style.display = 'none';
|
|
864
|
-
});
|
|
865
|
-
audioCol.appendChild(li);
|
|
866
|
-
});
|
|
867
|
-
|
|
868
|
-
props.subtitles.forEach(sub => {
|
|
869
|
-
const li = document.createElement('li');
|
|
870
|
-
li.innerHTML = `
|
|
871
|
-
<span data-lang="${ sub.lang }">${sub.label || sub.lang.toUpperCase()}</span>
|
|
872
|
-
<span class="icon">${renderIcon()}</span>
|
|
873
|
-
`;
|
|
874
|
-
|
|
875
|
-
if (sub.lang === currentSubtitleLang.value) li.classList.add('active');
|
|
876
|
-
li.addEventListener('click', () => {
|
|
877
|
-
Array.from(video.value?.textTracks || []).forEach(track => {
|
|
878
|
-
const tLang = (track.language || track.srclang || '').toLowerCase();
|
|
879
|
-
track.mode = tLang === sub.lang.toLowerCase() ? 'showing' : 'disabled';
|
|
880
|
-
});
|
|
881
|
-
subCol.querySelectorAll('li').forEach(el => el.classList.remove('active'));
|
|
882
|
-
li.classList.add('active');
|
|
883
|
-
menu.style.display = 'none';
|
|
884
|
-
currentSubtitleLang.value = sub.lang;
|
|
885
|
-
});
|
|
886
|
-
subCol.appendChild(li);
|
|
887
|
-
});
|
|
623
|
+
const observer = new MutationObserver(mutationObserver)
|
|
624
|
+
// --- Start pointer-update loop ---
|
|
625
|
+
if (!rafId) {
|
|
626
|
+
emitPointerUpdate()
|
|
627
|
+
}
|
|
628
|
+
observer.observe(document.querySelector('.video-player-theme-container'), { childList: true, subtree: true });
|
|
629
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
630
|
+
}
|
|
631
|
+
}
|
|
888
632
|
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
633
|
+
const mutationObserver = (mutationsList, observer) => {
|
|
634
|
+
const mediaTheme = document.querySelector('.video-player-theme-container');
|
|
635
|
+
if (mediaTheme && mediaTheme.shadowRoot) {
|
|
636
|
+
// controllbar
|
|
637
|
+
const controlBar = mediaTheme.shadowRoot.querySelector('media-control-bar');
|
|
638
|
+
const fullscreenButton = mediaTheme.shadowRoot.querySelector('media-fullscreen-button');
|
|
639
|
+
buttonElement = fullscreenButton
|
|
640
|
+
const playbackRateButton = mediaTheme.shadowRoot.querySelector('media-playback-rate-menu');
|
|
641
|
+
playbackRateButton.setAttribute('rates', '0.25 0.5 0.75 1 1.5 2 3');
|
|
642
|
+
if (fullscreenButton) {
|
|
643
|
+
fullscreenButton.handleClick = async (event) => {
|
|
644
|
+
event.preventDefault();
|
|
645
|
+
event.stopPropagation();
|
|
646
|
+
event.stopImmediatePropagation();
|
|
647
|
+
startFullscreen();
|
|
648
|
+
}
|
|
649
|
+
// --- Remove default CC button ---
|
|
650
|
+
const ccButton = mediaTheme.shadowRoot.querySelector('media-captions-button');
|
|
651
|
+
if (ccButton) {
|
|
652
|
+
ccButton.remove();
|
|
653
|
+
}
|
|
654
|
+
// --- Amazon Prime Style Language Switcher ---
|
|
655
|
+
if (controlBar && !controlBar.querySelector('.lang-switcher')
|
|
656
|
+
&& (props.multiLangSources.length > 1 || props.subtitles.length > 1)) {
|
|
657
|
+
const langDiv = document.createElement('div');
|
|
658
|
+
langDiv.className = 'lang-switcher';
|
|
659
|
+
langDiv.innerHTML = `
|
|
660
|
+
<style>
|
|
661
|
+
.lang-switcher {
|
|
662
|
+
position: relative;
|
|
663
|
+
display: flex;
|
|
664
|
+
align-items: center;
|
|
665
|
+
justify-content: center;
|
|
666
|
+
margin-left: var(--media-control-spacing, 6px);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
.lang-switcher button {
|
|
670
|
+
all: unset;
|
|
671
|
+
display: flex;
|
|
672
|
+
align-items: center;
|
|
673
|
+
justify-content: center;
|
|
674
|
+
cursor: pointer;
|
|
675
|
+
height: var(--media-button-height, 32px);
|
|
676
|
+
border-radius: 25%;
|
|
677
|
+
min-width: var(--media-button-height, 32px);
|
|
678
|
+
padding: var(--media-button-padding, 0 5px);
|
|
679
|
+
background: transparent;
|
|
680
|
+
transition: background 0.15s ease, transform 0.1s ease;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
.lang-switcher button svg {
|
|
684
|
+
width: var(--media-icon-size, 24px);
|
|
685
|
+
height: var(--media-icon-size, 24px);
|
|
686
|
+
color: var(--media-icon-color, white);
|
|
687
|
+
stroke: currentColor;
|
|
688
|
+
opacity: var(--media-icon-opacity, 0.9);
|
|
689
|
+
pointer-events: none;
|
|
690
|
+
transition: opacity 0.15s ease;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
.lang-switcher button:hover svg {
|
|
694
|
+
transform: scale(1.1);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
.lang-switcher button:hover {
|
|
698
|
+
opacity: var(--media-icon-opacity-hover, 1);
|
|
699
|
+
background: var(--media-control-hover-background, rgba(50 50 70 / .7));
|
|
700
|
+
transition: backdrop-filter 0.3s, -webkit-backdrop-filter 0.3s;
|
|
701
|
+
box-shadow: rgba(0, 0, 0, 0.3) 0px 0px 5px;
|
|
702
|
+
backdrop-filter: blur(10px) invert(15%) brightness(80%) opacity(1);
|
|
703
|
+
padding: 5px;
|
|
704
|
+
color: var(--media-text-color, var(--media-primary-color, rgb(238 238 238)));
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
.lang-btn[title]:hover::after {
|
|
708
|
+
content: attr(title);
|
|
709
|
+
position: fixed;
|
|
710
|
+
bottom: 122%;
|
|
711
|
+
right: -100%;
|
|
712
|
+
white-space: nowrap;
|
|
713
|
+
background: var(--media-tooltip-background, rgba(0,0,0,0.2));
|
|
714
|
+
color: var(--media-tooltip-color, #fff);
|
|
715
|
+
border-radius: 4px;
|
|
716
|
+
padding: 4px 15px;
|
|
717
|
+
font-size: var(--media-font-size, 12px);
|
|
718
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.4);
|
|
719
|
+
line-height: calc(1.2 * var(--base));
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
.lang-menu {
|
|
723
|
+
position: absolute;
|
|
724
|
+
bottom: calc(var(--media-button-height, 32px) * 1.7);
|
|
725
|
+
right: -48px;
|
|
726
|
+
background: var(--media-control-bar-background, rgba(20,20,20,0.4));
|
|
727
|
+
backdrop-filter: blur(12px);
|
|
728
|
+
border-radius: 12px;
|
|
729
|
+
box-shadow: rgba(0, 0, 0, 0.3) 0px 0px 5px;
|
|
730
|
+
padding: 10px;
|
|
731
|
+
min-width: 240px;
|
|
732
|
+
display: none;
|
|
733
|
+
animation: fadeUp 0.15s ease-out;
|
|
734
|
+
color: var(--media-text-color, #fff);
|
|
735
|
+
font-family: var(--media-font-family, system-ui, sans-serif);
|
|
736
|
+
font-size: var(--media-font-size, 13px);
|
|
737
|
+
z-index: 9999;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
@keyframes fadeUp {
|
|
741
|
+
from { opacity: 0; transform: translateY(8px); }
|
|
742
|
+
to { opacity: 1; transform: translateY(0); }
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
.lang-columns {
|
|
746
|
+
display: flex;
|
|
747
|
+
justify-content: space-between;
|
|
748
|
+
gap: 1rem;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
.lang-col {
|
|
752
|
+
flex: 1;
|
|
753
|
+
display: flex;
|
|
754
|
+
flex-direction: column;
|
|
755
|
+
margin: 0;
|
|
756
|
+
padding: 0;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
.lang-col .title {
|
|
760
|
+
line-height: calc(1.2 * var(--base));
|
|
761
|
+
font-weight: 600;
|
|
762
|
+
font-size: calc(var(--media-font-size, 13px) - 1px);
|
|
763
|
+
margin-bottom: 4px;
|
|
764
|
+
border-bottom: 1px solid rgba(255,255,255,0.1);
|
|
765
|
+
padding-bottom: 2px;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
.lang-col ul {
|
|
769
|
+
list-style: none;
|
|
770
|
+
margin: 0;
|
|
771
|
+
padding: 0;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
.lang-col li {
|
|
775
|
+
display: flex;
|
|
776
|
+
align-items: center;
|
|
777
|
+
justify-content: space-between;
|
|
778
|
+
cursor: pointer;
|
|
779
|
+
border-radius: 4px;
|
|
780
|
+
padding: 2px 12px;
|
|
781
|
+
transition: background 0.15s ease, color 0.15s ease;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
.lang-col li span {
|
|
785
|
+
line-height: calc(1.2 * var(--base));
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
.lang-col li:hover {
|
|
789
|
+
background: var(--media-control-hover-background, rgba(255,255,255,0.15));
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
.lang-col li.active {
|
|
793
|
+
font-weight: 500;
|
|
794
|
+
color: var(--media-accent-color, #fff);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
.lang-col li .icon {
|
|
798
|
+
opacity: 0;
|
|
799
|
+
transition: opacity 0.15s ease, transform 0.15s ease;
|
|
800
|
+
transform: translateX(2px);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
.lang-col li.active .icon {
|
|
804
|
+
opacity: 1;
|
|
805
|
+
transform: translateX(0);
|
|
806
|
+
}
|
|
807
|
+
</style>
|
|
808
|
+
|
|
809
|
+
<button title="Audio & Subtitles" class="lang-btn">
|
|
810
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 26 26" fill="none" stroke="currentColor" stroke-width="1.5"
|
|
811
|
+
stroke-linecap="round" stroke-linejoin="round">
|
|
812
|
+
<path d="m5 8 6 6"/>
|
|
813
|
+
<path d="m4 14 6-6 2-3"/>
|
|
814
|
+
<path d="M2 5h12"/>
|
|
815
|
+
<path d="M7 2h1"/>
|
|
816
|
+
<path d="m22 22-5-10-5 10"/>
|
|
817
|
+
<path d="M14 18h6"/>
|
|
818
|
+
</svg>
|
|
819
|
+
</button>
|
|
820
|
+
|
|
821
|
+
<div class="lang-menu">
|
|
822
|
+
<div class="lang-columns">
|
|
823
|
+
<ul class="lang-col audio-col"><li class="title">Audio</li></ul>
|
|
824
|
+
<ul class="lang-col sub-col"><li class="title">Subtitles</li></ul>
|
|
825
|
+
</div>
|
|
826
|
+
</div>
|
|
827
|
+
`;
|
|
828
|
+
const menu = langDiv.querySelector('.lang-menu');
|
|
829
|
+
const audioCol = menu.querySelector('.audio-col');
|
|
830
|
+
const subCol = menu.querySelector('.sub-col');
|
|
831
|
+
const button = langDiv.querySelector('button');
|
|
832
|
+
menu.addEventListener('click', e => e.stopPropagation());
|
|
833
|
+
// Chrome Media Player style check icon
|
|
834
|
+
const renderIcon = () => `
|
|
835
|
+
<svg aria-hidden="true" viewBox="0 1 24 24" width="16" height="16" part="checked-indicator indicator">
|
|
836
|
+
<path fill="currentColor" d="m10 15.17 9.193-9.191 1.414 1.414-10.606 10.606-6.364-6.364 1.414-1.414 4.95 4.95Z"></path>
|
|
837
|
+
</svg>
|
|
838
|
+
`;
|
|
839
|
+
|
|
840
|
+
// Audio options
|
|
841
|
+
props.multiLangSources.forEach(src => {
|
|
842
|
+
const li = document.createElement('li');
|
|
843
|
+
li.innerHTML = `
|
|
844
|
+
<span data-lang="${ src.lang }">${src.label || src.lang.toUpperCase()}</span>
|
|
845
|
+
<span class="icon">${renderIcon()}</span>
|
|
846
|
+
`;
|
|
847
|
+
if (src.lang === currentLang.value) li.classList.add('active');
|
|
848
|
+
li.addEventListener('click', () => {
|
|
849
|
+
audioCol.querySelectorAll('li').forEach(el => el.classList.remove('active'));
|
|
850
|
+
li.classList.add('active');
|
|
851
|
+
selectLang(src.lang);
|
|
852
|
+
menu.style.display = 'none';
|
|
853
|
+
});
|
|
854
|
+
audioCol.appendChild(li);
|
|
855
|
+
});
|
|
856
|
+
props.subtitles.forEach(sub => {
|
|
857
|
+
const li = document.createElement('li');
|
|
858
|
+
li.innerHTML = `
|
|
859
|
+
<span data-lang="${ sub.lang }">${sub.label || sub.lang.toUpperCase()}</span>
|
|
860
|
+
<span class="icon">${renderIcon()}</span>
|
|
861
|
+
`;
|
|
862
|
+
|
|
863
|
+
if (sub.lang === currentSubtitleLang.value) li.classList.add('active');
|
|
864
|
+
li.addEventListener('click', () => {
|
|
865
|
+
Array.from(video.value?.textTracks || []).forEach(track => {
|
|
866
|
+
const tLang = (track.language || track.srclang || '').toLowerCase();
|
|
867
|
+
track.mode = tLang === sub.lang.toLowerCase() ? 'showing' : 'disabled';
|
|
901
868
|
});
|
|
869
|
+
subCol.querySelectorAll('li').forEach(el => el.classList.remove('active'));
|
|
870
|
+
li.classList.add('active');
|
|
871
|
+
menu.style.display = 'none';
|
|
872
|
+
currentSubtitleLang.value = sub.lang;
|
|
873
|
+
});
|
|
874
|
+
subCol.appendChild(li);
|
|
875
|
+
});
|
|
902
876
|
|
|
903
|
-
|
|
877
|
+
button.addEventListener('click', e => {
|
|
878
|
+
e.stopPropagation();
|
|
879
|
+
const isOpen = menu.style.display === 'block';
|
|
880
|
+
menu.style.display = isOpen ? 'none' : 'block';
|
|
881
|
+
updateLangMenuState();
|
|
882
|
+
});
|
|
883
|
+
document.addEventListener('click', e => {
|
|
884
|
+
if (!menu.contains(e.target) && !button.contains(e.target)) {
|
|
885
|
+
menu.style.display = 'none';
|
|
886
|
+
button.setAttribute('aria-expanded', 'false');
|
|
887
|
+
updateLangMenuState();
|
|
904
888
|
}
|
|
889
|
+
});
|
|
905
890
|
|
|
906
|
-
|
|
907
|
-
} else {
|
|
908
|
-
console.error('Button not found in Shadow DOM!');
|
|
909
|
-
}
|
|
910
|
-
} else {
|
|
911
|
-
console.error('Shadow Root not found!');
|
|
891
|
+
controlBar.insertBefore(langDiv, fullscreenButton);
|
|
912
892
|
}
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
emitPointerUpdate()
|
|
893
|
+
|
|
894
|
+
observer.disconnect();
|
|
895
|
+
} else {
|
|
896
|
+
console.error('Button not found in Shadow DOM!');
|
|
918
897
|
}
|
|
919
|
-
|
|
898
|
+
} else {
|
|
899
|
+
console.error('Shadow Root not found!');
|
|
920
900
|
}
|
|
921
901
|
}
|
|
922
902
|
|
|
@@ -938,16 +918,6 @@ function changeSpeed(e) {
|
|
|
938
918
|
video.value.playbackRate = video.value.playbackRate - 0.25
|
|
939
919
|
}
|
|
940
920
|
}
|
|
941
|
-
defineExpose({ startFullscreen, getHls: () => hls, getVideo: () => video.value,
|
|
942
|
-
changeSource: (newLink) => {
|
|
943
|
-
if (!newLink) return;
|
|
944
|
-
hls.stopLoad();
|
|
945
|
-
hls.detachMedia();
|
|
946
|
-
hls.loadSource(newLink);
|
|
947
|
-
hls.attachMedia(video.value);
|
|
948
|
-
hls.startLoad();
|
|
949
|
-
}
|
|
950
|
-
});
|
|
951
921
|
|
|
952
922
|
</script>
|
|
953
923
|
<style>
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
:autoplay="autoplay"
|
|
10
10
|
:isControls="isControls"
|
|
11
11
|
:onVideoEnd="onVideoEnd"
|
|
12
|
+
:subtitles="subtitles"
|
|
12
13
|
:additionHeaders="additionHeaders"
|
|
13
14
|
:isFullscreen="isFullscreen"
|
|
14
15
|
:showTranscriptBlock="showTranscriptBlock"
|
|
@@ -88,6 +89,10 @@ const props = defineProps({
|
|
|
88
89
|
additionHeaders: {
|
|
89
90
|
type: Object,
|
|
90
91
|
default: {}
|
|
92
|
+
},
|
|
93
|
+
subtitles: {
|
|
94
|
+
type: Array,
|
|
95
|
+
default: []
|
|
91
96
|
}
|
|
92
97
|
})
|
|
93
98
|
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
:isFullscreen="isFullscreen"
|
|
12
12
|
:link="link"
|
|
13
13
|
:progress="progress"
|
|
14
|
+
:subtitles="subtitles"
|
|
14
15
|
:isMuted="isMuted"
|
|
15
16
|
:autoplay="autoplay"
|
|
16
17
|
v-model="videoElement"
|
|
@@ -98,6 +99,10 @@ const props = defineProps({
|
|
|
98
99
|
additionHeaders: {
|
|
99
100
|
type: Object,
|
|
100
101
|
default: {}
|
|
102
|
+
},
|
|
103
|
+
subtitles: {
|
|
104
|
+
type: Array,
|
|
105
|
+
default: []
|
|
101
106
|
}
|
|
102
107
|
})
|
|
103
108
|
|