@grfzhl/vue-hls-player 1.1.25 → 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 +4 -0
- package/dist/VideoPlayer/BasePlayer.vue +209 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -207,6 +207,10 @@ 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.
|
|
210
214
|
v1.1.25
|
|
211
215
|
- Decouple audio language switching from subtitle selection.
|
|
212
216
|
- Preserve user-selected subtitle language across audio changes and HLS source reloads.
|
|
@@ -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"
|
|
@@ -215,6 +216,12 @@ const isUserInitiatedLangChange = ref(false)
|
|
|
215
216
|
|
|
216
217
|
let initialLoad = true;
|
|
217
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)
|
|
218
225
|
|
|
219
226
|
watch(
|
|
220
227
|
() => props.defaultLang,
|
|
@@ -349,7 +356,12 @@ function emitPointerUpdate() {
|
|
|
349
356
|
const videoElement = defineModel()
|
|
350
357
|
|
|
351
358
|
onMounted(() => {
|
|
352
|
-
|
|
359
|
+
if (video.value) {
|
|
360
|
+
video.value.muted = mutedAttr.value
|
|
361
|
+
video.value.currentTime = props.progress
|
|
362
|
+
}
|
|
363
|
+
initVideo()
|
|
364
|
+
maybeLoadFirstFrame()
|
|
353
365
|
})
|
|
354
366
|
|
|
355
367
|
onUnmounted(() => {
|
|
@@ -384,23 +396,26 @@ const currentSubtitle = computed(() => {
|
|
|
384
396
|
return props.subtitles[0];
|
|
385
397
|
})
|
|
386
398
|
|
|
387
|
-
watch(() => props.autoplay, (
|
|
388
|
-
if(
|
|
389
|
-
|
|
390
|
-
a[1].muted = true
|
|
391
|
-
setTimeout(() => {
|
|
392
|
-
a[1].play().catch(err => console.warn("Autoplay-Error:", err));
|
|
393
|
-
}, 200)
|
|
399
|
+
watch(() => props.autoplay, (autoplay) => {
|
|
400
|
+
if (!autoplay || !video.value || !video.value.paused) {
|
|
401
|
+
return
|
|
394
402
|
}
|
|
403
|
+
|
|
404
|
+
video.value.muted = true
|
|
405
|
+
setTimeout(() => {
|
|
406
|
+
handleInitialPlay().catch(err => console.warn("Autoplay-Error:", err))
|
|
407
|
+
}, 200)
|
|
395
408
|
})
|
|
396
409
|
|
|
397
410
|
watch(
|
|
398
411
|
() => props.link,
|
|
399
412
|
(newLink, oldLink) => {
|
|
400
|
-
|
|
401
|
-
|
|
413
|
+
if (newLink !== oldLink) {
|
|
414
|
+
resetPlayer()
|
|
415
|
+
maybeLoadFirstFrame()
|
|
416
|
+
}
|
|
402
417
|
}
|
|
403
|
-
|
|
418
|
+
)
|
|
404
419
|
|
|
405
420
|
async function startFullscreen() {
|
|
406
421
|
let vpVideoBlock = document.getElementById(props.fullScreenElement);
|
|
@@ -547,13 +562,130 @@ function toggleTranscript() {
|
|
|
547
562
|
props.showTranscriptBlock = !props.showTranscriptBlock
|
|
548
563
|
}
|
|
549
564
|
|
|
550
|
-
function
|
|
551
|
-
|
|
552
|
-
|
|
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
|
+
}
|
|
553
685
|
}
|
|
554
686
|
|
|
555
|
-
function prepareVideoPlayer(link) {
|
|
556
|
-
if (!video.value) return;
|
|
687
|
+
function prepareVideoPlayer(link, { previewOnly = false } = {}) {
|
|
688
|
+
if (!video.value || !link) return;
|
|
557
689
|
|
|
558
690
|
// Reset previous HLS instance
|
|
559
691
|
if (hls) {
|
|
@@ -561,15 +693,68 @@ function prepareVideoPlayer(link) {
|
|
|
561
693
|
hls.destroy();
|
|
562
694
|
}
|
|
563
695
|
|
|
696
|
+
const playerHlsConfig = {
|
|
697
|
+
...hlsConfig,
|
|
698
|
+
autoStartLoad: !previewOnly,
|
|
699
|
+
};
|
|
700
|
+
|
|
564
701
|
// Preparing video player with link: ${link}
|
|
565
|
-
hls = new Hls(
|
|
702
|
+
hls = new Hls(playerHlsConfig);
|
|
566
703
|
// Attach HLS
|
|
567
704
|
hls.loadSource(link);
|
|
568
705
|
hls.attachMedia(video.value);
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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
|
+
}
|
|
573
758
|
});
|
|
574
759
|
// Native subtitle handling – without polling
|
|
575
760
|
Array.from(video.value?.textTracks || []).forEach(track => {
|
|
@@ -615,7 +800,6 @@ function prepareVideoPlayer(link) {
|
|
|
615
800
|
video.value.currentTime = props.progress;
|
|
616
801
|
// Chrome-like: update menu whenever track mode changes
|
|
617
802
|
video.value.textTracks.addEventListener('change', updateLangMenuState);
|
|
618
|
-
initVideo(); // Init controls etc.
|
|
619
803
|
}
|
|
620
804
|
|
|
621
805
|
|