@grfzhl/vue-hls-player 1.1.24 → 1.1.26
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 +8 -0
- package/dist/VideoPlayer/BasePlayer.vue +250 -62
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -207,6 +207,14 @@ At the moment the following attribute are supported:
|
|
|
207
207
|
```
|
|
208
208
|
|
|
209
209
|
### Last release:
|
|
210
|
+
v1.1.26
|
|
211
|
+
- Prevent HLS chunk preloading during player initialization.
|
|
212
|
+
- Start HLS loading only after user interaction.
|
|
213
|
+
- Fallback to the first video frame when no preview image is available.
|
|
214
|
+
v1.1.25
|
|
215
|
+
- Decouple audio language switching from subtitle selection.
|
|
216
|
+
- Preserve user-selected subtitle language across audio changes and HLS source reloads.
|
|
217
|
+
- Prevent unintended subtitle resets caused by audio language updates.
|
|
210
218
|
v1.1.24
|
|
211
219
|
- Add user-initiated language-changed emit.
|
|
212
220
|
- Fix unwanted language re-sync on player init.
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
</div>
|
|
9
9
|
<div class="media-overlay" v-if="initialPlayButton">
|
|
10
10
|
<div class="initial-play" :class="{'hide-playbutton': hideInitialPlayButton}">
|
|
11
|
-
<media-play-button mediapaused="" class="media-button" aria-label="play" tabindex="0" role="button" @click="
|
|
11
|
+
<media-play-button mediapaused="" class="media-button" aria-label="play" tabindex="0" role="button" @click="handleInitialPlay">
|
|
12
12
|
<svg slot="icon" viewBox="0 0 32 32">
|
|
13
13
|
<g>
|
|
14
14
|
<path id="icon-play" d="M20.7131 14.6976C21.7208 15.2735 21.7208 16.7265 20.7131 17.3024L12.7442 21.856C11.7442 22.4274 10.5 21.7054 10.5 20.5536L10.5 11.4464C10.5 10.2946 11.7442 9.57257 12.7442 10.144L20.7131 14.6976Z"></path>
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
ref="video"
|
|
32
32
|
:poster="previewImageLink"
|
|
33
33
|
:controls="false"
|
|
34
|
+
preload="none"
|
|
34
35
|
:title="title"
|
|
35
36
|
controlslist="nodownload"
|
|
36
37
|
playsinline
|
|
@@ -44,7 +45,7 @@
|
|
|
44
45
|
<track
|
|
45
46
|
v-if="subtitles.length"
|
|
46
47
|
v-for="(subtitle, i) in subtitles"
|
|
47
|
-
:key="subtitle.lang + '-' +
|
|
48
|
+
:key="subtitle.lang + '-' + currentAudioLang"
|
|
48
49
|
:src="subtitle.link"
|
|
49
50
|
kind="subtitles"
|
|
50
51
|
:srclang="subtitle.lang"
|
|
@@ -71,7 +72,7 @@
|
|
|
71
72
|
<slot name="between-video-and-transcript"></slot>
|
|
72
73
|
<slot name="before-transcripts"></slot>
|
|
73
74
|
<SubtitleBlock
|
|
74
|
-
:key="`${
|
|
75
|
+
:key="`${currentAudioLang}-${currentSubtitleLang}`"
|
|
75
76
|
ref="transcriptRef"
|
|
76
77
|
:subtitle="currentSubtitle"
|
|
77
78
|
:cursor="videoCursor"
|
|
@@ -185,35 +186,56 @@ let currentTime = 0
|
|
|
185
186
|
let hls = null
|
|
186
187
|
let buttonElement = null
|
|
187
188
|
// --- lang switcher ---
|
|
188
|
-
const
|
|
189
|
+
const currentAudioLang = ref(props.defaultLang || 'en')
|
|
190
|
+
|
|
191
|
+
const sessionSubtitleLang = 'vp_subtitle_lang'
|
|
192
|
+
function initSubtitleLang() {
|
|
193
|
+
const stored = sessionStorage.getItem(sessionSubtitleLang)
|
|
194
|
+
|
|
195
|
+
if (stored && props.subtitles.some(s => s.lang === stored)) {
|
|
196
|
+
currentSubtitleLang.value = stored
|
|
197
|
+
} else {
|
|
198
|
+
currentSubtitleLang.value =
|
|
199
|
+
props.subtitles.find(s => s.lang === props.defaultLang)?.lang
|
|
200
|
+
|| props.subtitles[0]?.lang
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
applySubtitleTrack(currentSubtitleLang.value)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function applySubtitleTrack(lang) {
|
|
207
|
+
Array.from(video.value?.textTracks || []).forEach(track => {
|
|
208
|
+
const tLang = (track.language || track.srclang || '').toLowerCase()
|
|
209
|
+
track.mode = tLang === lang.toLowerCase() ? 'showing' : 'disabled'
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
|
|
189
215
|
const isUserInitiatedLangChange = ref(false)
|
|
190
216
|
|
|
191
217
|
let initialLoad = true;
|
|
218
|
+
let defaultApplied = false
|
|
219
|
+
const hlsInitialized = ref(false)
|
|
220
|
+
const hlsInitializing = ref(false)
|
|
221
|
+
const pendingPlayRequest = ref(false)
|
|
222
|
+
const pendingSeekTime = ref(null)
|
|
223
|
+
const previewFramePrimed = ref(false)
|
|
224
|
+
const previewFrameLoading = ref(false)
|
|
192
225
|
|
|
193
226
|
watch(
|
|
194
227
|
() => props.defaultLang,
|
|
195
|
-
(
|
|
196
|
-
if (
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (newLang && newLang !== oldLang && newLang !== currentLang.value) {
|
|
202
|
-
const hasSub = props.subtitles?.find(s => s.lang === newLang);
|
|
203
|
-
if (hasSub) {
|
|
204
|
-
currentSubtitleLang.value = newLang;
|
|
205
|
-
Array.from(video.value?.textTracks || []).forEach(track => {
|
|
206
|
-
const tLang = (track.language || track.srclang || '').toLowerCase();
|
|
207
|
-
track.mode = tLang === newLang.toLowerCase() ? 'showing' : 'disabled';
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
}
|
|
228
|
+
(lang) => {
|
|
229
|
+
if (defaultApplied) return
|
|
230
|
+
currentAudioLang.value = lang
|
|
231
|
+
defaultApplied = true
|
|
211
232
|
}
|
|
212
233
|
)
|
|
234
|
+
|
|
213
235
|
// --- Remember and restore last subtitle language ---
|
|
214
236
|
async function selectLang(lang) {
|
|
215
237
|
if (!video.value || !hls) return;
|
|
216
|
-
if (lang ===
|
|
238
|
+
if (lang === currentAudioLang.value) return;
|
|
217
239
|
|
|
218
240
|
const newSource = props.multiLangSources?.find(s => s.lang === lang);
|
|
219
241
|
if (!newSource?.file_url) {
|
|
@@ -227,7 +249,7 @@ async function selectLang(lang) {
|
|
|
227
249
|
const wasMuted = video.value.muted;
|
|
228
250
|
const rate = video.value.playbackRate;
|
|
229
251
|
|
|
230
|
-
|
|
252
|
+
currentAudioLang.value = lang;
|
|
231
253
|
// Switching to ${lang}...
|
|
232
254
|
|
|
233
255
|
try {
|
|
@@ -284,7 +306,7 @@ function updateLangMenuState() {
|
|
|
284
306
|
const langElement = li.querySelector('span[data-lang]')
|
|
285
307
|
const liLang = langElement?.dataset?.lang;
|
|
286
308
|
// const liLang = li.textContent.trim().toLowerCase();
|
|
287
|
-
li.classList.toggle('active', liLang ===
|
|
309
|
+
li.classList.toggle('active', liLang === currentAudioLang.value.toLowerCase());
|
|
288
310
|
});
|
|
289
311
|
|
|
290
312
|
// --- Subtitles ---
|
|
@@ -334,7 +356,12 @@ function emitPointerUpdate() {
|
|
|
334
356
|
const videoElement = defineModel()
|
|
335
357
|
|
|
336
358
|
onMounted(() => {
|
|
337
|
-
|
|
359
|
+
if (video.value) {
|
|
360
|
+
video.value.muted = mutedAttr.value
|
|
361
|
+
video.value.currentTime = props.progress
|
|
362
|
+
}
|
|
363
|
+
initVideo()
|
|
364
|
+
maybeLoadFirstFrame()
|
|
338
365
|
})
|
|
339
366
|
|
|
340
367
|
onUnmounted(() => {
|
|
@@ -369,23 +396,26 @@ const currentSubtitle = computed(() => {
|
|
|
369
396
|
return props.subtitles[0];
|
|
370
397
|
})
|
|
371
398
|
|
|
372
|
-
watch(() => props.autoplay, (
|
|
373
|
-
if(
|
|
374
|
-
|
|
375
|
-
a[1].muted = true
|
|
376
|
-
setTimeout(() => {
|
|
377
|
-
a[1].play().catch(err => console.warn("Autoplay-Error:", err));
|
|
378
|
-
}, 200)
|
|
399
|
+
watch(() => props.autoplay, (autoplay) => {
|
|
400
|
+
if (!autoplay || !video.value || !video.value.paused) {
|
|
401
|
+
return
|
|
379
402
|
}
|
|
403
|
+
|
|
404
|
+
video.value.muted = true
|
|
405
|
+
setTimeout(() => {
|
|
406
|
+
handleInitialPlay().catch(err => console.warn("Autoplay-Error:", err))
|
|
407
|
+
}, 200)
|
|
380
408
|
})
|
|
381
409
|
|
|
382
410
|
watch(
|
|
383
411
|
() => props.link,
|
|
384
412
|
(newLink, oldLink) => {
|
|
385
|
-
|
|
386
|
-
|
|
413
|
+
if (newLink !== oldLink) {
|
|
414
|
+
resetPlayer()
|
|
415
|
+
maybeLoadFirstFrame()
|
|
416
|
+
}
|
|
387
417
|
}
|
|
388
|
-
|
|
418
|
+
)
|
|
389
419
|
|
|
390
420
|
async function startFullscreen() {
|
|
391
421
|
let vpVideoBlock = document.getElementById(props.fullScreenElement);
|
|
@@ -532,13 +562,130 @@ function toggleTranscript() {
|
|
|
532
562
|
props.showTranscriptBlock = !props.showTranscriptBlock
|
|
533
563
|
}
|
|
534
564
|
|
|
535
|
-
function
|
|
536
|
-
|
|
537
|
-
|
|
565
|
+
async function handleInitialPlay() {
|
|
566
|
+
await startPlayback()
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
async function startPlayback() {
|
|
570
|
+
if (!video.value) return
|
|
571
|
+
|
|
572
|
+
pendingPlayRequest.value = true
|
|
573
|
+
pendingSeekTime.value = null
|
|
574
|
+
initialPlayButton.value = false
|
|
575
|
+
|
|
576
|
+
if (!hlsInitialized.value) {
|
|
577
|
+
initPlayer(props.link)
|
|
578
|
+
return
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (previewFrameLoading.value && !previewFramePrimed.value) {
|
|
582
|
+
return
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
resumePreviewFrameStream()
|
|
586
|
+
|
|
587
|
+
try {
|
|
588
|
+
await video.value.play()
|
|
589
|
+
pendingPlayRequest.value = false
|
|
590
|
+
} catch (err) {
|
|
591
|
+
console.warn('[HLS] Play start failed:', err)
|
|
592
|
+
initialPlayButton.value = true
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function initPlayer(src, { previewOnly = false } = {}) {
|
|
597
|
+
if (!video.value || !src || hlsInitialized.value || hlsInitializing.value) return
|
|
598
|
+
|
|
599
|
+
hlsInitializing.value = true
|
|
600
|
+
|
|
601
|
+
try {
|
|
602
|
+
prepareVideoPlayer(src, { previewOnly })
|
|
603
|
+
hlsInitialized.value = true
|
|
604
|
+
} finally {
|
|
605
|
+
hlsInitializing.value = false
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function maybeLoadFirstFrame() {
|
|
610
|
+
if (
|
|
611
|
+
props.previewImageLink ||
|
|
612
|
+
!props.link ||
|
|
613
|
+
!video.value ||
|
|
614
|
+
hlsInitialized.value ||
|
|
615
|
+
hlsInitializing.value
|
|
616
|
+
) {
|
|
617
|
+
return
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
initPlayer(props.link, { previewOnly: true })
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function resumePreviewFrameStream() {
|
|
624
|
+
if (!previewFramePrimed.value || !hls || !video.value) return
|
|
625
|
+
|
|
626
|
+
previewFramePrimed.value = false
|
|
627
|
+
hls.startLoad(video.value.currentTime || 0)
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function resetPlayer() {
|
|
631
|
+
hlsInitialized.value = false
|
|
632
|
+
hlsInitializing.value = false
|
|
633
|
+
pendingPlayRequest.value = false
|
|
634
|
+
pendingSeekTime.value = null
|
|
635
|
+
previewFramePrimed.value = false
|
|
636
|
+
previewFrameLoading.value = false
|
|
637
|
+
initialPlayButton.value = true
|
|
638
|
+
hideInitialPlayButton.value = false
|
|
639
|
+
|
|
640
|
+
if (hls) {
|
|
641
|
+
hls.detachMedia()
|
|
642
|
+
hls.destroy()
|
|
643
|
+
hls = null
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (!video.value) return
|
|
647
|
+
|
|
648
|
+
video.value.pause()
|
|
649
|
+
video.value.removeAttribute('src')
|
|
650
|
+
const source = video.value.querySelector('source')
|
|
651
|
+
if (source) {
|
|
652
|
+
source.removeAttribute('src')
|
|
653
|
+
}
|
|
654
|
+
video.value.load()
|
|
655
|
+
video.value.currentTime = props.progress
|
|
656
|
+
video.value.muted = mutedAttr.value
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
async function seekVideo(time) {
|
|
660
|
+
if (!video.value) return
|
|
661
|
+
|
|
662
|
+
if (!hlsInitialized.value) {
|
|
663
|
+
pendingSeekTime.value = time
|
|
664
|
+
pendingPlayRequest.value = true
|
|
665
|
+
initPlayer(props.link)
|
|
666
|
+
return
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (previewFrameLoading.value && !previewFramePrimed.value) {
|
|
670
|
+
pendingSeekTime.value = time
|
|
671
|
+
pendingPlayRequest.value = true
|
|
672
|
+
return
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
resumePreviewFrameStream()
|
|
676
|
+
video.value.currentTime = time
|
|
677
|
+
|
|
678
|
+
try {
|
|
679
|
+
await video.value.play()
|
|
680
|
+
pendingPlayRequest.value = false
|
|
681
|
+
pendingSeekTime.value = null
|
|
682
|
+
} catch (err) {
|
|
683
|
+
console.warn('[HLS] Seek play failed:', err)
|
|
684
|
+
}
|
|
538
685
|
}
|
|
539
686
|
|
|
540
|
-
function prepareVideoPlayer(link) {
|
|
541
|
-
if (!video.value) return;
|
|
687
|
+
function prepareVideoPlayer(link, { previewOnly = false } = {}) {
|
|
688
|
+
if (!video.value || !link) return;
|
|
542
689
|
|
|
543
690
|
// Reset previous HLS instance
|
|
544
691
|
if (hls) {
|
|
@@ -546,15 +693,68 @@ function prepareVideoPlayer(link) {
|
|
|
546
693
|
hls.destroy();
|
|
547
694
|
}
|
|
548
695
|
|
|
696
|
+
const playerHlsConfig = {
|
|
697
|
+
...hlsConfig,
|
|
698
|
+
autoStartLoad: !previewOnly,
|
|
699
|
+
};
|
|
700
|
+
|
|
549
701
|
// Preparing video player with link: ${link}
|
|
550
|
-
hls = new Hls(
|
|
702
|
+
hls = new Hls(playerHlsConfig);
|
|
551
703
|
// Attach HLS
|
|
552
704
|
hls.loadSource(link);
|
|
553
705
|
hls.attachMedia(video.value);
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
706
|
+
hls.once(Hls.Events.MANIFEST_PARSED, async () => {
|
|
707
|
+
if (!video.value) return;
|
|
708
|
+
|
|
709
|
+
video.value.muted = mutedAttr.value;
|
|
710
|
+
video.value.currentTime = pendingSeekTime.value ?? props.progress;
|
|
711
|
+
|
|
712
|
+
if (previewOnly) {
|
|
713
|
+
previewFrameLoading.value = true;
|
|
714
|
+
video.value.addEventListener('loadeddata', async () => {
|
|
715
|
+
previewFrameLoading.value = false;
|
|
716
|
+
|
|
717
|
+
if (!video.value || !hls) return;
|
|
718
|
+
|
|
719
|
+
video.value.pause();
|
|
720
|
+
|
|
721
|
+
if (!pendingPlayRequest.value) {
|
|
722
|
+
previewFramePrimed.value = true;
|
|
723
|
+
hls.stopLoad();
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
previewFramePrimed.value = false;
|
|
728
|
+
hls.startLoad(video.value.currentTime || 0);
|
|
729
|
+
|
|
730
|
+
if (pendingSeekTime.value != null) {
|
|
731
|
+
video.value.currentTime = pendingSeekTime.value;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
try {
|
|
735
|
+
await video.value.play();
|
|
736
|
+
pendingPlayRequest.value = false;
|
|
737
|
+
pendingSeekTime.value = null;
|
|
738
|
+
} catch (err) {
|
|
739
|
+
console.warn('[HLS] Play after first frame failed:', err);
|
|
740
|
+
initialPlayButton.value = true;
|
|
741
|
+
}
|
|
742
|
+
}, { once: true });
|
|
743
|
+
|
|
744
|
+
hls.startLoad(0);
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
if (!pendingPlayRequest.value) return;
|
|
749
|
+
|
|
750
|
+
try {
|
|
751
|
+
await video.value.play();
|
|
752
|
+
pendingPlayRequest.value = false;
|
|
753
|
+
pendingSeekTime.value = null;
|
|
754
|
+
} catch (err) {
|
|
755
|
+
console.warn('[HLS] Play after manifest failed:', err);
|
|
756
|
+
initialPlayButton.value = true;
|
|
757
|
+
}
|
|
558
758
|
});
|
|
559
759
|
// Native subtitle handling – without polling
|
|
560
760
|
Array.from(video.value?.textTracks || []).forEach(track => {
|
|
@@ -589,14 +789,7 @@ function prepareVideoPlayer(link) {
|
|
|
589
789
|
|
|
590
790
|
// Initialize subtitles
|
|
591
791
|
if (props.subtitles?.length > 0) {
|
|
592
|
-
|
|
593
|
-
currentSubtitleLang.value = defaultSub ? props.defaultLang : props.subtitles[0].lang;
|
|
594
|
-
Array.from(video.value?.textTracks || []).forEach(track => {
|
|
595
|
-
const tLang = (track.language || track.srclang || '').toLowerCase();
|
|
596
|
-
const shouldShow = tLang === currentSubtitleLang.value.toLowerCase();
|
|
597
|
-
track.mode = shouldShow ? 'showing' : 'disabled';
|
|
598
|
-
// console.log('[SubtitleInit] Track found:', tLang, '->', shouldShow ? 'showing' : 'disabled');
|
|
599
|
-
});
|
|
792
|
+
initSubtitleLang();
|
|
600
793
|
}
|
|
601
794
|
|
|
602
795
|
selectLang(props.defaultLang);
|
|
@@ -607,7 +800,6 @@ function prepareVideoPlayer(link) {
|
|
|
607
800
|
video.value.currentTime = props.progress;
|
|
608
801
|
// Chrome-like: update menu whenever track mode changes
|
|
609
802
|
video.value.textTracks.addEventListener('change', updateLangMenuState);
|
|
610
|
-
initVideo(); // Init controls etc.
|
|
611
803
|
}
|
|
612
804
|
|
|
613
805
|
|
|
@@ -875,7 +1067,7 @@ const mutationObserver = (mutationsList, observer) => {
|
|
|
875
1067
|
<span data-lang="${ src.lang }">${src.label || src.lang.toUpperCase()}</span>
|
|
876
1068
|
<span class="icon">${renderIcon()}</span>
|
|
877
1069
|
`;
|
|
878
|
-
if (src.lang ===
|
|
1070
|
+
if (src.lang === currentAudioLang.value) li.classList.add('active');
|
|
879
1071
|
li.addEventListener('click', () => {
|
|
880
1072
|
audioCol.querySelectorAll('li').forEach(el => el.classList.remove('active'));
|
|
881
1073
|
li.classList.add('active');
|
|
@@ -894,14 +1086,10 @@ const mutationObserver = (mutationsList, observer) => {
|
|
|
894
1086
|
|
|
895
1087
|
if (sub.lang === currentSubtitleLang.value) li.classList.add('active');
|
|
896
1088
|
li.addEventListener('click', () => {
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
subCol.querySelectorAll('li').forEach(el => el.classList.remove('active'));
|
|
902
|
-
li.classList.add('active');
|
|
903
|
-
menu.style.display = 'none';
|
|
904
|
-
currentSubtitleLang.value = sub.lang;
|
|
1089
|
+
currentSubtitleLang.value = sub.lang
|
|
1090
|
+
sessionStorage.setItem(sessionSubtitleLang, sub.lang)
|
|
1091
|
+
applySubtitleTrack(sub.lang)
|
|
1092
|
+
menu.style.display = 'none'
|
|
905
1093
|
});
|
|
906
1094
|
subCol.appendChild(li);
|
|
907
1095
|
});
|