@grfzhl/vue-hls-player 1.0.3 → 1.0.5

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
@@ -120,6 +120,16 @@ it can show and hide the video control panel
120
120
  subtitles to add as tracks to the video
121
121
 
122
122
  ### Last release:
123
+ v1.0.5
124
+ 1. Load transcriptions additionally to subtitles
125
+ 2. Add styled transcription block for better readability
126
+ 3. Improve interaction and dynamic params
127
+
128
+ v1.0.4
129
+ 1. Make subtitles dynamic
130
+ 2. Add new switch to disable the subtitle block
131
+ 3. Fix some minor issues
132
+
123
133
  v1.0.3
124
134
  1. Removed controls in favour of themable overlay by `player.style`.
125
135
  2. Updated hls library
@@ -1,15 +1,18 @@
1
1
  <template>
2
2
  <div class="video-container">
3
- <media-theme-sutro>
3
+ <media-theme-sutro class="video-player-theme-container">
4
4
  <video
5
+ class="hls-player"
5
6
  slot="media"
6
7
  @pause="pause"
7
- @ended="pause"
8
8
  @keyup="changeSpeed"
9
+ @ended="onVideoEnd"
10
+ @seek="seekVideo"
9
11
  ref="video"
10
12
  :poster="previewImageLink"
11
13
  :controls="false"
12
14
  :title="title"
15
+ :isFullscreen="isFullscreen"
13
16
  controlslist="nodownload"
14
17
  playsinline
15
18
  crossorigin
@@ -27,13 +30,21 @@
27
30
  :label="subtitle.label" :default="i === 0" />
28
31
  </video>
29
32
  </media-theme-sutro>
30
- <div ref="subtitlesContainer" class="custom-subtitles"></div>
33
+ <div class="custom-subtitles" v-show="!showTranscriptBlock">
34
+ <div class="subtitle-text" ref="subtitlesContainer" style="display: none;"></div>
35
+ </div>
31
36
  </div>
32
- <SubtitleBlock :vttFile="subtitles[0]" :cursor="videoCursor"></SubtitleBlock>
37
+ <SubtitleBlock
38
+ :subtitle="currentSubtitle"
39
+ :cursor="videoCursor"
40
+ :showTranscriptBlock="showTranscriptBlock"
41
+ @seek="seekVideo"
42
+ @toggleTranscript="toggleTranscript">
43
+ </SubtitleBlock>
33
44
  </template>
34
45
 
35
46
  <script setup>
36
- import { onMounted, onUpdated, ref, onUnmounted } from 'vue'
47
+ import { onMounted, onUpdated, ref, onUnmounted, computed } from 'vue'
37
48
  import Hls from 'hls.js'
38
49
  import 'player.style/sutro';
39
50
  import SubtitleBlock from './SubtitleBlock.vue';
@@ -67,18 +78,35 @@ const props = defineProps({
67
78
  subtitles: {
68
79
  type: Array,
69
80
  default: []
81
+ },
82
+ /**
83
+ * true, if showing separate
84
+ * block with transcripts
85
+ */
86
+ showTranscriptBlock: {
87
+ type: Boolean,
88
+ default: true
89
+ },
90
+ isFullscreen: {
91
+ type: Boolean,
92
+ default: false
70
93
  }
71
94
  })
72
95
 
73
- const emit = defineEmits(['pause', 'test'])
96
+ const emit = defineEmits(['pause', 'video-ended', 'video-fullscreen-change'])
74
97
  const video = ref(null)
75
98
  const subtitlesContainer = ref(null)
99
+ const currentSubtitleLang = ref(null)
76
100
  const videoCursor = ref(0)
101
+ const isFullscreen = ref(false);
77
102
 
78
103
  onMounted(() => {
104
+ console.log("mounted current - - changed")
79
105
  prepareVideoPlayer()
80
106
  if (video.value) {
107
+ checkFullscreen();
81
108
  video.value.addEventListener('timeupdate', updateCurrentTime);
109
+ document.addEventListener('fullscreenchange', onFullscreenChange);
82
110
  }
83
111
  })
84
112
 
@@ -88,13 +116,43 @@ onUpdated(() => {
88
116
  onUnmounted(() => {
89
117
  if (video.value) {
90
118
  video.value.removeEventListener('timeupdate', updateCurrentTime);
119
+ document.removeEventListener('fullscreenchange', onFullscreenChange);
91
120
  }
92
121
  });
93
122
 
123
+ const currentSubtitle = computed(() => {
124
+ if(props.subtitles) {
125
+ const current = props.subtitles.filter((subt) => {
126
+ return subt.lang === currentSubtitleLang.value
127
+ })
128
+ return current.length ? current[0] : null
129
+ }
130
+ return null
131
+ })
132
+
133
+ function checkFullscreen() {
134
+ isFullscreen.value = document.fullscreenElement === video.value;
135
+ };
136
+
137
+ function onFullscreenChange() {
138
+ checkFullscreen();
139
+ emit('video-fullscreen-change', isFullscreen)
140
+ };
141
+
142
+
94
143
  function updateCurrentTime() {
95
144
  videoCursor.value = video.value.currentTime;
96
145
  }
97
146
 
147
+ function toggleTranscript() {
148
+ props.showTranscriptBlock = !props.showTranscriptBlock
149
+ }
150
+
151
+ function seekVideo(time) {
152
+ video.value.currentTime = time;
153
+ video.play()
154
+ }
155
+
98
156
  function prepareVideoPlayer() {
99
157
  let hls = new Hls()
100
158
  let stream = props.link
@@ -111,6 +169,7 @@ function prepareVideoPlayer() {
111
169
  Array.from(textTracks).forEach((track, index) => {
112
170
  track.addEventListener("cuechange", () => {
113
171
  const activeCues = track.activeCues;
172
+ currentSubtitleLang.value = track.language
114
173
  if (activeCues && activeCues.length > 0) {
115
174
  subtitlesContainer.value.textContent = activeCues[0].text
116
175
  subtitlesContainer.value.style.display = "block";
@@ -119,9 +178,9 @@ function prepareVideoPlayer() {
119
178
  }
120
179
  });
121
180
  if (track.mode !== previousModes[index]) {
122
- console.log(`Track mode changed: ${track.mode}`);
123
181
  if (track.mode === "showing") {
124
182
  const activeCues = track.activeCues;
183
+ currentSubtitleLang.value = track.language
125
184
  if (activeCues && activeCues.length > 0) {
126
185
  subtitlesContainer.value.style.display = "block";
127
186
  subtitlesContainer.value.textContent = activeCues[0].text
@@ -141,10 +200,15 @@ function prepareVideoPlayer() {
141
200
 
142
201
  function pause() {
143
202
  const currentTime = video?.value?.currentTime || 0
144
-
145
203
  emit('pause', currentTime)
146
204
  }
147
205
 
206
+ function onVideoEnd() {
207
+ const currentTime = video?.value?.currentTime || 0
208
+ pause()
209
+ emit('video-ended', { currentTime: currentTime, video });
210
+ }
211
+
148
212
  function changeSpeed(e) {
149
213
  if (e.key === 'w' && video && video.value) {
150
214
  video.value.playbackRate = video.value.playbackRate + 0.25
@@ -165,17 +229,26 @@ function changeSpeed(e) {
165
229
  position: absolute;
166
230
  left: 50%;
167
231
  width: auto;
168
- max-width: 90%;
232
+ max-width: 95%;
169
233
  text-align: center;
170
234
  background: rgba(0, 0, 0, 0.7);
235
+ border-radius: 6px;
236
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
237
+ transform: translate(-50%) translateY(calc(-100% - 60px));
238
+ }
239
+ .custom-subtitles .subtitle-text {
171
240
  color: white;
172
- font-size: 16px;
241
+ font-size: 14px;
173
242
  font-family: Arial, sans-serif;
174
243
  line-height: 1.5;
175
- padding: 10px 20px;
176
- border-radius: 10px;
177
- box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
178
- margin-top: -120px;
179
- transform: translateX(-50%) translateY(-100%);
244
+ padding: 8px 10px;
245
+ }
246
+
247
+ .video-player-theme-container, .hls-player {
248
+ width: 100%;
249
+ }
250
+
251
+ .video-container {
252
+ position: relative;
180
253
  }
181
254
  </style>
@@ -1,73 +1,246 @@
1
1
  <template>
2
- <div class="transcript-container"
3
- ref="subtitlesContainer">
4
- <ul v-if="cues.length" class="subtitles">
5
- <li v-for="cue of cues" :class="{'current-highlight': cursor >= cue.start && cursor <= cue.end}">
6
- <span class="seconds">{{ secondsToTime(cue.start) }} - {{ secondsToTime(cue.end) }}</span>
7
- <span class="text">{{ cue.text }}</span>
2
+ <div class="transcript-container" ref="subtitlesContainer">
3
+ <div class="transcript-toggle">
4
+ <button data-headlessui-state="open" @click="toggleTranscript()">
5
+ <div class="icon">
6
+ <svg v-if="!showTranscriptBlock" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-right-icon duration-200 h-4 w-4 text-gray-900 stroke-1"><path d="m9 18 6-6-6-6"></path></svg>
7
+ <svg v-if="showTranscriptBlock" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-right-icon rotate-90 transform duration-200 h-4 w-4 text-gray-900 stroke-1"><path d="m9 18 6-6-6-6"></path></svg>
8
+ </div>
9
+ Transcript
10
+ </button>
11
+ </div>
12
+ <ul v-if="txtCues.length && showTranscriptBlock" class="subtitles">
13
+ <li
14
+ v-for="(txtCue, index) in txtCues"
15
+ :key="index"
16
+ :class="{ 'current-highlight': isTxtCueActive(txtCue) }"
17
+ @click="seekTo(txtCue.start)"
18
+ >
19
+ <div class="play-icon">
20
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-play lucide transform duration-200 h-4 w-4 text-gray-900 stroke-1"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="m9 8 6 4-6 4Z"/></svg>
21
+ </div>
22
+ <div class="content">
23
+ <span class="meta">
24
+ <span class="seconds">{{ secondsToTime(txtCue.start) }} - {{ secondsToTime(txtCue.end) }}</span>
25
+ <span class="narrator">{{ txtCue.dialog[0].speaker }}</span>
26
+ </span>
27
+ <span class="text">
28
+ <span
29
+ v-for="(word, wordIndex) in txtCue.dialog[0].text.split('')"
30
+ :key="wordIndex"
31
+ :class="{ 'active-word': isWordActive(txtCue, word, wordIndex, index) && isTxtCueActive(txtCue) }"
32
+ >
33
+ {{ word }}
34
+ </span>
35
+ </span>
36
+ </div>
8
37
  </li>
9
38
  </ul>
10
39
  </div>
11
40
  </template>
12
41
 
13
42
  <style lang="css" scoped>
14
- .subtitles li.current-highlight {
15
- font-weight: bold;
16
- }
43
+ .transcript-toggle {
44
+ font-weight: bold;
45
+ }
46
+ .transcript-toggle button {
47
+ display: flex;
48
+ }
49
+ .transcript-toggle button .icon {
50
+ padding: 3px;
51
+ }
52
+ .transcript-container {
53
+ max-height: 400px;
54
+ overflow-y: scroll;
55
+ border-radius: 8px;
56
+ border: 1px solid var(--outline-gray-1);
57
+ padding: 10px;
58
+ font-family: Arial, sans-serif;
59
+ }
60
+
61
+ .subtitles {
62
+ list-style: none;
63
+ padding: 0;
64
+ margin: 0;
65
+ }
66
+
67
+ .subtitles li {
68
+ padding: 5px;
69
+ cursor: pointer;
70
+ display: flex;
71
+ }
72
+
73
+ .subtitles li .play-icon {
74
+ min-width: 24px;
75
+ padding-top: 4px;
76
+ }
77
+
78
+ .subtitles li .content .meta {
79
+ display: flex;
80
+ padding-bottom: 4px;
81
+ }
82
+ .subtitles li .content .meta .seconds {
83
+ font-weight: 500;
84
+ }
85
+
86
+ .subtitles li .content .meta .narrator {
87
+ padding-left: 10px;
88
+ font-weight: 500;
89
+ }
90
+
91
+ .subtitles li.current-highlight {
92
+ }
93
+
94
+ .subtitles li .active-word {
95
+ background-color: var(--outline-gray-1);
96
+ }
97
+
98
+ .subtitles li .seconds {
99
+ display: block;
100
+ }
101
+
102
+ .subtitles li .text .narrator {
103
+ display: block;
104
+ color: #333;
105
+ }
17
106
  </style>
18
107
 
19
108
  <script setup>
20
- import { onMounted, onUpdated, ref } from 'vue'
109
+ import { onMounted, watch, ref } from 'vue';
21
110
 
22
111
  const props = defineProps({
23
- vttFile: {
24
- type: String,
25
- default: ''
112
+ subtitle: {
113
+ type: Object,
114
+ default: null,
26
115
  },
27
116
  cursor: {
28
117
  type: Number,
29
- default: 0
118
+ default: 0,
30
119
  },
31
- })
120
+ showTranscriptBlock: {
121
+ type: Boolean,
122
+ default: true
123
+ }
124
+ });
125
+
126
+ const emit = defineEmits(['seek', 'toggleTranscript']);
127
+ const subtitlesContainer = ref(null);
128
+ const vttCues = ref([]);
129
+ const txtCues = ref([]);
130
+ const currentCue = ref(null);
32
131
 
33
- const emit = defineEmits(['pause', 'test'])
34
- const subtitlesContainer = ref(null)
35
- const cues = ref([])
36
132
 
37
- onMounted(async () => {
38
- cues.value = await parseVTT(props.vttFile.link)
39
- console.log(cues.value)
40
- })
133
+ const loadCues = async () => {
134
+ if (props.subtitle) {
135
+ const vttPath = props.subtitle.link;
136
+ const txtPath = vttPath.replace(/\.vtt$/, '.txt');
41
137
 
42
- onUpdated(async () => {
43
- // auto scroll text
44
- if(subtitlesContainer) {
45
- const activeSubtitle = subtitlesContainer?.value?.querySelectorAll('li.current-highlight')[0];
138
+ vttCues.value = await parseVTT(vttPath);
139
+ txtCues.value = await parseTXT(txtPath);
140
+ }
141
+ };
142
+
143
+ onMounted(loadCues);
144
+
145
+ watch(
146
+ () => props.subtitle,
147
+ async (newSubtitle) => {
148
+ if (newSubtitle) await loadCues();
149
+ }
150
+ );
151
+
152
+ watch(
153
+ () => props.cursor,
154
+ (currentTime) => {
155
+ highlightActiveCue(currentTime);
156
+ checkCurrentCue(currentTime)
157
+ }
158
+ );
159
+
160
+ /**
161
+ * highlgiht the current transcript part
162
+ * and keep it scrolled up
163
+ * @param currentTime
164
+ */
165
+ function highlightActiveCue(currentTime) {
166
+ if (subtitlesContainer.value) {
167
+ const activeSubtitle = subtitlesContainer.value.querySelectorAll('li.current-highlight')[0];
46
168
  if (activeSubtitle) {
47
- subtitlesContainer.value.scrollTop = activeSubtitle.offsetTop - subtitlesContainer.value.offsetTop;
169
+ subtitlesContainer.value.scrollTop =
170
+ activeSubtitle.offsetTop - subtitlesContainer.value.offsetTop;
48
171
  }
49
172
  }
50
- })
173
+ }
51
174
 
175
+ function toggleTranscript() {
176
+ emit('toggleTranscript', null)
177
+ }
178
+
179
+ function seekTo(time) {
180
+ emit('seek', time);
181
+ }
182
+
183
+ /**
184
+ * returns true, if the given txt
185
+ * is currently played
186
+ * @param txtCue
187
+ */
188
+ function isTxtCueActive(txtCue) {
189
+ return props.cursor >= txtCue.start && props.cursor <= txtCue.end;
190
+ }
191
+
192
+ /**
193
+ * true if the word part can be highlighed
194
+ * as currently active
195
+ * @param txtCue
196
+ * @param word
197
+ * @param wordIndex
198
+ * @param txtIndex
199
+ */
200
+ function isWordActive(txtCue, word, wordIndex, txtIndex) {
201
+ if(!currentCue.value || !word || !txtCue) {
202
+ return false
203
+ }
204
+ const startPos = txtCue.dialog[0].text.indexOf(currentCue.value)
205
+ const endPos = startPos + currentCue.value.length
206
+ if(wordIndex >= startPos && wordIndex < endPos) {
207
+ return true;
208
+ }
209
+ return false;
210
+ }
52
211
 
212
+ function checkCurrentCue(currentCursor) {
213
+ Array.from(vttCues.value).forEach((a, index) => {
214
+ if(currentCursor >= a.start && currentCursor <= a.end) {
215
+ currentCue.value = a.text
216
+ }
217
+ });
218
+ }
219
+
220
+ /**
221
+ * get subtitles from the
222
+ * vtt files
223
+ * @param fileUrl
224
+ */
53
225
  async function parseVTT(fileUrl) {
54
226
  const response = await fetch(fileUrl);
55
227
  const text = await response.text();
56
228
  const cues = [];
57
- const lines = text.split("\n");
229
+ const lines = text.split('\n');
58
230
  let cue = null;
59
231
 
60
232
  for (let i = 0; i < lines.length; i++) {
61
233
  const line = lines[i].trim();
62
234
  if (!line) continue;
63
235
 
64
- if (line.includes("-->")) {
65
- const [start, end] = line.split(" --> ");
66
- cue = { start: timeToSeconds(start), end: timeToSeconds(end), text: "" };
236
+ if (line.includes('-->')) {
237
+ const [start, end] = line.split(' --> ');
238
+ cue = { start: timeToSeconds(start), end: timeToSeconds(end), text: '' };
67
239
  } else if (cue) {
68
- cue.text += line + " ";
240
+ cue.text += line + ' ';
69
241
  }
70
- if (cue && (!lines[i + 1] || lines[i + 1].includes("-->"))) {
242
+
243
+ if (cue && (!lines[i + 1] || lines[i + 1].includes('-->'))) {
71
244
  cues.push(cue);
72
245
  cue = null;
73
246
  }
@@ -75,8 +248,54 @@ async function parseVTT(fileUrl) {
75
248
  return cues;
76
249
  }
77
250
 
251
+ /**
252
+ * get the raw transcriptions
253
+ * from the txt files
254
+ * @param fileUrl
255
+ */
256
+ async function parseTXT(fileUrl) {
257
+ const response = await fetch(fileUrl);
258
+ const text = await response.text();
259
+ const cues = [];
260
+ const lines = text.split('\n');
261
+ let cue = null;
262
+ let dialog = null;
263
+ for (let i = 0; i < lines.length; i++) {
264
+ const line = lines[i].trim();
265
+ if (!line) continue;
266
+
267
+ /**
268
+ * extract every transcript part by time
269
+ */
270
+ if (line.match(/^\d{2}:\d{2}:\d{2}:\d{2} - \d{2}:\d{2}:\d{2}:\d{2}/)) {
271
+ const [start, end] = line.split(' - ');
272
+ cue = {
273
+ start: timeToSeconds(start),
274
+ end: timeToSeconds(end),
275
+ dialog: []
276
+ };
277
+ dialog = {
278
+ text: '',
279
+ speaker: ''
280
+ }
281
+ } else if (cue && dialog.text == '' && dialog.speaker == '') {
282
+ dialog.speaker = line;
283
+ } else if (cue && dialog.speaker !== '') {
284
+ dialog.text += line + ' ';
285
+ }
286
+
287
+ if (cue && (!lines[i + 1] || lines[i + 1].match(/^\d{2}:\d{2}:\d{2}:\d{2} - \d{2}:\d{2}:\d{2}:\d{2}/))) {
288
+ cue.dialog.push(dialog);
289
+ cues.push(cue);
290
+ cue = null;
291
+ }
292
+ }
293
+ return cues;
294
+ }
295
+
296
+ // Hilfsfunktionen
78
297
  function timeToSeconds(timestamp) {
79
- const [hours, minutes, seconds] = timestamp.split(":").map(parseFloat);
298
+ const [hours, minutes, seconds] = timestamp.split(':').map(parseFloat);
80
299
  return hours * 3600 + minutes * 60 + seconds;
81
300
  }
82
301
 
@@ -87,7 +306,4 @@ function secondsToTime(seconds) {
87
306
  const pad = (num) => String(num).padStart(2, '0');
88
307
  return `${pad(hours)}:${pad(minutes)}:${pad(secs)}`;
89
308
  }
90
- </script>
91
-
92
-
93
-
309
+ </script>
@@ -5,14 +5,19 @@
5
5
  :progress="progress"
6
6
  :isMuted="isMuted"
7
7
  :isControls="isControls"
8
+ :onVideoEnd="onVideoEnd"
9
+ :isFullscreen="isFullscreen"
10
+ :showTranscriptBlock="showTranscriptBlock"
8
11
  @pause="pause"
12
+ @video-ended="onVideoEnd"
13
+ @video-fullscreen-change="onFullscreenChange"
9
14
  />
10
15
  </template>
11
16
 
12
17
  <script setup>
13
18
  import BasePlayer from './BasePlayer.vue'
14
19
 
15
- const emit = defineEmits(['pause'])
20
+ const emit = defineEmits(['pause', 'video-ended', 'video-fullscreen-change'])
16
21
 
17
22
  defineProps({
18
23
  previewImageLink: {
@@ -35,9 +40,26 @@ defineProps({
35
40
  type: Boolean,
36
41
  default: true
37
42
  },
43
+ isFullscreen: {
44
+ type: Boolean,
45
+ default: false
46
+ },
47
+ showTranscriptBlock: {
48
+ type: Boolean,
49
+ default: true
50
+ }
38
51
  })
39
52
 
40
53
  function pause(currentTime) {
41
54
  emit('pause', currentTime)
42
55
  }
56
+
57
+ function onVideoEnd(data) {
58
+ pause()
59
+ emit('video-ended', data);
60
+ }
61
+
62
+ function onFullscreenChange(data) {
63
+ emit('video-fullscreen-change', data);
64
+ }
43
65
  </script>
@@ -2,11 +2,14 @@
2
2
  <VDefaultVideoPlayer
3
3
  v-if="type === 'default'"
4
4
  @pause="pause"
5
+ @video-fullscreen-change="onVideoFullScreenChange"
6
+ @video-ended="onVideoEnd"
5
7
  :previewImageLink="previewImageLink"
8
+ :showTranscriptBlock="showTranscriptBlock"
9
+ :isFullscreen="isFullscreen"
6
10
  :link="link"
7
11
  :progress="progress"
8
12
  :isMuted="isMuted"
9
- :isControls="isControls"
10
13
  />
11
14
 
12
15
  <VPreviewVideoPlayer
@@ -20,7 +23,7 @@
20
23
  import VDefaultVideoPlayer from './VDefaultVideoPlayer.vue'
21
24
  import VPreviewVideoPlayer from './VPreviewVideoPlayer.vue'
22
25
 
23
- const emit = defineEmits(['pause'])
26
+ const emit = defineEmits(['pause', 'video-ended', 'video-fullscreen-change'])
24
27
 
25
28
  defineProps({
26
29
  previewImageLink: {
@@ -47,9 +50,24 @@ defineProps({
47
50
  type: Boolean,
48
51
  default: true
49
52
  },
53
+ isFullscreen: {
54
+ type: Boolean,
55
+ default: false
56
+ },
57
+ showTranscriptBlock: {
58
+ type: Boolean,
59
+ default: true
60
+ }
50
61
  })
62
+ console.log("abc <<<<<<<<<<<<<")
51
63
 
52
64
  function pause(currentTime) {
53
65
  emit('pause', currentTime)
54
66
  }
67
+ function onVideoFullScreenChange(data) {
68
+ emit('video-fullscreen-change', data)
69
+ }
70
+ function onVideoEnd(data) {
71
+ emit('video-ended', data);
72
+ }
55
73
  </script>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@grfzhl/vue-hls-player",
3
3
  "private": false,
4
- "version": "1.0.3",
4
+ "version": "1.0.5",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "dist"