@grfzhl/vue-hls-player 1.0.4 → 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,11 @@ 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
+
123
128
  v1.0.4
124
129
  1. Make subtitles dynamic
125
130
  2. Add new switch to disable the subtitle block
@@ -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,9 +30,17 @@
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 v-if="showSubtitleBlock" :subtitle="currentSubtitle" :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>
@@ -72,22 +83,30 @@ const props = defineProps({
72
83
  * true, if showing separate
73
84
  * block with transcripts
74
85
  */
75
- showSubtitleBlock: {
86
+ showTranscriptBlock: {
76
87
  type: Boolean,
77
88
  default: true
89
+ },
90
+ isFullscreen: {
91
+ type: Boolean,
92
+ default: false
78
93
  }
79
94
  })
80
95
 
81
- const emit = defineEmits(['pause', 'test'])
96
+ const emit = defineEmits(['pause', 'video-ended', 'video-fullscreen-change'])
82
97
  const video = ref(null)
83
98
  const subtitlesContainer = ref(null)
84
99
  const currentSubtitleLang = ref(null)
85
100
  const videoCursor = ref(0)
101
+ const isFullscreen = ref(false);
86
102
 
87
103
  onMounted(() => {
104
+ console.log("mounted current - - changed")
88
105
  prepareVideoPlayer()
89
106
  if (video.value) {
107
+ checkFullscreen();
90
108
  video.value.addEventListener('timeupdate', updateCurrentTime);
109
+ document.addEventListener('fullscreenchange', onFullscreenChange);
91
110
  }
92
111
  })
93
112
 
@@ -97,6 +116,7 @@ onUpdated(() => {
97
116
  onUnmounted(() => {
98
117
  if (video.value) {
99
118
  video.value.removeEventListener('timeupdate', updateCurrentTime);
119
+ document.removeEventListener('fullscreenchange', onFullscreenChange);
100
120
  }
101
121
  });
102
122
 
@@ -105,16 +125,34 @@ const currentSubtitle = computed(() => {
105
125
  const current = props.subtitles.filter((subt) => {
106
126
  return subt.lang === currentSubtitleLang.value
107
127
  })
108
- console.log("found current", current)
109
128
  return current.length ? current[0] : null
110
129
  }
111
130
  return null
112
131
  })
113
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
+
114
143
  function updateCurrentTime() {
115
144
  videoCursor.value = video.value.currentTime;
116
145
  }
117
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
+
118
156
  function prepareVideoPlayer() {
119
157
  let hls = new Hls()
120
158
  let stream = props.link
@@ -162,10 +200,15 @@ function prepareVideoPlayer() {
162
200
 
163
201
  function pause() {
164
202
  const currentTime = video?.value?.currentTime || 0
165
-
166
203
  emit('pause', currentTime)
167
204
  }
168
205
 
206
+ function onVideoEnd() {
207
+ const currentTime = video?.value?.currentTime || 0
208
+ pause()
209
+ emit('video-ended', { currentTime: currentTime, video });
210
+ }
211
+
169
212
  function changeSpeed(e) {
170
213
  if (e.key === 'w' && video && video.value) {
171
214
  video.value.playbackRate = video.value.playbackRate + 0.25
@@ -186,17 +229,26 @@ function changeSpeed(e) {
186
229
  position: absolute;
187
230
  left: 50%;
188
231
  width: auto;
189
- max-width: 90%;
232
+ max-width: 95%;
190
233
  text-align: center;
191
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 {
192
240
  color: white;
193
- font-size: 16px;
241
+ font-size: 14px;
194
242
  font-family: Arial, sans-serif;
195
243
  line-height: 1.5;
196
- padding: 10px 20px;
197
- border-radius: 10px;
198
- box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
199
- margin-top: -120px;
200
- 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;
201
253
  }
202
254
  </style>
@@ -1,77 +1,291 @@
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
112
  subtitle: {
24
113
  type: Object,
25
- default: null
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
+ });
32
125
 
33
- const emit = defineEmits(['pause', 'test'])
34
- const subtitlesContainer = ref(null)
35
- const cues = ref([])
126
+ const emit = defineEmits(['seek', 'toggleTranscript']);
127
+ const subtitlesContainer = ref(null);
128
+ const vttCues = ref([]);
129
+ const txtCues = ref([]);
130
+ const currentCue = ref(null);
36
131
 
37
- onMounted(async () => {
38
- if(props.subtitle) {
39
- cues.value = await parseVTT(props.subtitle.link)
132
+
133
+ const loadCues = async () => {
134
+ if (props.subtitle) {
135
+ const vttPath = props.subtitle.link;
136
+ const txtPath = vttPath.replace(/\.vtt$/, '.txt');
137
+
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();
40
149
  }
41
- })
150
+ );
42
151
 
43
- onUpdated(async () => {
44
- if(props.subtitle) {
45
- cues.value = await parseVTT(props.subtitle.link)
152
+ watch(
153
+ () => props.cursor,
154
+ (currentTime) => {
155
+ highlightActiveCue(currentTime);
156
+ checkCurrentCue(currentTime)
46
157
  }
47
- // auto scroll text
48
- if(subtitlesContainer) {
49
- const activeSubtitle = subtitlesContainer?.value?.querySelectorAll('li.current-highlight')[0];
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];
50
168
  if (activeSubtitle) {
51
- subtitlesContainer.value.scrollTop = activeSubtitle.offsetTop - subtitlesContainer.value.offsetTop;
169
+ subtitlesContainer.value.scrollTop =
170
+ activeSubtitle.offsetTop - subtitlesContainer.value.offsetTop;
52
171
  }
53
172
  }
54
- })
173
+ }
55
174
 
175
+ function toggleTranscript() {
176
+ emit('toggleTranscript', null)
177
+ }
56
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
+ }
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
+ */
57
225
  async function parseVTT(fileUrl) {
58
226
  const response = await fetch(fileUrl);
59
227
  const text = await response.text();
60
228
  const cues = [];
61
- const lines = text.split("\n");
229
+ const lines = text.split('\n');
62
230
  let cue = null;
63
231
 
64
232
  for (let i = 0; i < lines.length; i++) {
65
233
  const line = lines[i].trim();
66
234
  if (!line) continue;
67
235
 
68
- if (line.includes("-->")) {
69
- const [start, end] = line.split(" --> ");
70
- 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: '' };
71
239
  } else if (cue) {
72
- cue.text += line + " ";
240
+ cue.text += line + ' ';
241
+ }
242
+
243
+ if (cue && (!lines[i + 1] || lines[i + 1].includes('-->'))) {
244
+ cues.push(cue);
245
+ cue = null;
246
+ }
247
+ }
248
+ return cues;
249
+ }
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 + ' ';
73
285
  }
74
- if (cue && (!lines[i + 1] || lines[i + 1].includes("-->"))) {
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);
75
289
  cues.push(cue);
76
290
  cue = null;
77
291
  }
@@ -79,8 +293,9 @@ async function parseVTT(fileUrl) {
79
293
  return cues;
80
294
  }
81
295
 
296
+ // Hilfsfunktionen
82
297
  function timeToSeconds(timestamp) {
83
- const [hours, minutes, seconds] = timestamp.split(":").map(parseFloat);
298
+ const [hours, minutes, seconds] = timestamp.split(':').map(parseFloat);
84
299
  return hours * 3600 + minutes * 60 + seconds;
85
300
  }
86
301
 
@@ -91,7 +306,4 @@ function secondsToTime(seconds) {
91
306
  const pad = (num) => String(num).padStart(2, '0');
92
307
  return `${pad(hours)}:${pad(minutes)}:${pad(secs)}`;
93
308
  }
94
- </script>
95
-
96
-
97
-
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,7 +2,11 @@
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"
@@ -19,7 +23,7 @@
19
23
  import VDefaultVideoPlayer from './VDefaultVideoPlayer.vue'
20
24
  import VPreviewVideoPlayer from './VPreviewVideoPlayer.vue'
21
25
 
22
- const emit = defineEmits(['pause'])
26
+ const emit = defineEmits(['pause', 'video-ended', 'video-fullscreen-change'])
23
27
 
24
28
  defineProps({
25
29
  previewImageLink: {
@@ -46,9 +50,24 @@ defineProps({
46
50
  type: Boolean,
47
51
  default: true
48
52
  },
53
+ isFullscreen: {
54
+ type: Boolean,
55
+ default: false
56
+ },
57
+ showTranscriptBlock: {
58
+ type: Boolean,
59
+ default: true
60
+ }
49
61
  })
62
+ console.log("abc <<<<<<<<<<<<<")
50
63
 
51
64
  function pause(currentTime) {
52
65
  emit('pause', currentTime)
53
66
  }
67
+ function onVideoFullScreenChange(data) {
68
+ emit('video-fullscreen-change', data)
69
+ }
70
+ function onVideoEnd(data) {
71
+ emit('video-ended', data);
72
+ }
54
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.4",
4
+ "version": "1.0.5",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "dist"