@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 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="video.play()">
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 + '-' + currentLang"
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="`${currentLang}-${currentSubtitleLang}`"
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
- prepareVideoPlayer(props.link)
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, (a) => {
388
- if(a[0].autoplay && a[1] && a[1].paused) {
389
- // autoplay is only possible when muted
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
- if (newLink !== oldLink) {
401
- prepareVideoPlayer(newLink);
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 seekVideo(time) {
551
- video.value.currentTime = time;
552
- video.value.play()
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(hlsConfig);
702
+ hls = new Hls(playerHlsConfig);
566
703
  // Attach HLS
567
704
  hls.loadSource(link);
568
705
  hls.attachMedia(video.value);
569
- video.value.textTracks.addEventListener('change', () => {
570
- Array.from(video.value?.textTracks || []).forEach(track => {
571
- console.log('[TrackChange] Track', track.language || track.srclang, '→', track.mode);
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
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@grfzhl/vue-hls-player",
3
3
  "private": false,
4
- "version": "1.1.25",
4
+ "version": "1.1.26",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "dist"