@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
|
|
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
|
|
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
|
-
|
|
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', '
|
|
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:
|
|
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:
|
|
241
|
+
font-size: 14px;
|
|
194
242
|
font-family: Arial, sans-serif;
|
|
195
243
|
line-height: 1.5;
|
|
196
|
-
padding: 10px
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
15
|
-
|
|
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,
|
|
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(['
|
|
34
|
-
const subtitlesContainer = ref(null)
|
|
35
|
-
const
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
152
|
+
watch(
|
|
153
|
+
() => props.cursor,
|
|
154
|
+
(currentTime) => {
|
|
155
|
+
highlightActiveCue(currentTime);
|
|
156
|
+
checkCurrentCue(currentTime)
|
|
46
157
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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>
|