@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 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="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"
@@ -185,35 +186,56 @@ let currentTime = 0
185
186
  let hls = null
186
187
  let buttonElement = null
187
188
  // --- lang switcher ---
188
- const currentLang = ref(props.defaultLang || 'en')
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
- (newLang, oldLang) => {
196
- if (initialLoad) {
197
- initialLoad = false;
198
- return;
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 === currentLang.value) return;
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
- currentLang.value = lang;
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 === currentLang.value.toLowerCase());
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
- prepareVideoPlayer(props.link)
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, (a) => {
373
- if(a[0].autoplay && a[1] && a[1].paused) {
374
- // autoplay is only possible when muted
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
- if (newLink !== oldLink) {
386
- prepareVideoPlayer(newLink);
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 seekVideo(time) {
536
- video.value.currentTime = time;
537
- 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
+ }
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(hlsConfig);
702
+ hls = new Hls(playerHlsConfig);
551
703
  // Attach HLS
552
704
  hls.loadSource(link);
553
705
  hls.attachMedia(video.value);
554
- video.value.textTracks.addEventListener('change', () => {
555
- Array.from(video.value?.textTracks || []).forEach(track => {
556
- console.log('[TrackChange] Track', track.language || track.srclang, '→', track.mode);
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
- const defaultSub = props.subtitles.find(s => s.lang === props.defaultLang);
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 === currentLang.value) li.classList.add('active');
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
- Array.from(video.value?.textTracks || []).forEach(track => {
898
- const tLang = (track.language || track.srclang || '').toLowerCase();
899
- track.mode = tLang === sub.lang.toLowerCase() ? 'showing' : 'disabled';
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
  });
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.24",
4
+ "version": "1.1.26",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "dist"