@grfzhl/vue-hls-player 1.0.4 → 1.0.6
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,11 +1,13 @@
|
|
|
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"
|
|
@@ -27,9 +29,17 @@
|
|
|
27
29
|
:label="subtitle.label" :default="i === 0" />
|
|
28
30
|
</video>
|
|
29
31
|
</media-theme-sutro>
|
|
30
|
-
<div
|
|
32
|
+
<div class="custom-subtitles" v-show="!showTranscriptBlock">
|
|
33
|
+
<div class="subtitle-text" ref="subtitlesContainer" style="display: none;"></div>
|
|
34
|
+
</div>
|
|
31
35
|
</div>
|
|
32
|
-
<SubtitleBlock
|
|
36
|
+
<SubtitleBlock
|
|
37
|
+
:subtitle="currentSubtitle"
|
|
38
|
+
:cursor="videoCursor"
|
|
39
|
+
:showTranscriptBlock="showTranscriptBlock"
|
|
40
|
+
@seek="seekVideo"
|
|
41
|
+
@toggleTranscript="toggleTranscript">
|
|
42
|
+
</SubtitleBlock>
|
|
33
43
|
</template>
|
|
34
44
|
|
|
35
45
|
<script setup>
|
|
@@ -72,22 +82,29 @@ const props = defineProps({
|
|
|
72
82
|
* true, if showing separate
|
|
73
83
|
* block with transcripts
|
|
74
84
|
*/
|
|
75
|
-
|
|
85
|
+
showTranscriptBlock: {
|
|
76
86
|
type: Boolean,
|
|
77
87
|
default: true
|
|
88
|
+
},
|
|
89
|
+
isFullscreen: {
|
|
90
|
+
type: Boolean,
|
|
91
|
+
default: false
|
|
78
92
|
}
|
|
79
93
|
})
|
|
80
94
|
|
|
81
|
-
const emit = defineEmits(['pause', '
|
|
95
|
+
const emit = defineEmits(['pause', 'video-ended', 'video-fullscreen-change'])
|
|
82
96
|
const video = ref(null)
|
|
83
97
|
const subtitlesContainer = ref(null)
|
|
84
98
|
const currentSubtitleLang = ref(null)
|
|
85
99
|
const videoCursor = ref(0)
|
|
100
|
+
const isFullscreen = ref(false);
|
|
86
101
|
|
|
87
102
|
onMounted(() => {
|
|
88
103
|
prepareVideoPlayer()
|
|
89
104
|
if (video.value) {
|
|
105
|
+
checkFullscreen();
|
|
90
106
|
video.value.addEventListener('timeupdate', updateCurrentTime);
|
|
107
|
+
document.addEventListener('fullscreenchange', onFullscreenChange);
|
|
91
108
|
}
|
|
92
109
|
})
|
|
93
110
|
|
|
@@ -97,6 +114,7 @@ onUpdated(() => {
|
|
|
97
114
|
onUnmounted(() => {
|
|
98
115
|
if (video.value) {
|
|
99
116
|
video.value.removeEventListener('timeupdate', updateCurrentTime);
|
|
117
|
+
document.removeEventListener('fullscreenchange', onFullscreenChange);
|
|
100
118
|
}
|
|
101
119
|
});
|
|
102
120
|
|
|
@@ -105,16 +123,34 @@ const currentSubtitle = computed(() => {
|
|
|
105
123
|
const current = props.subtitles.filter((subt) => {
|
|
106
124
|
return subt.lang === currentSubtitleLang.value
|
|
107
125
|
})
|
|
108
|
-
console.log("found current", current)
|
|
109
126
|
return current.length ? current[0] : null
|
|
110
127
|
}
|
|
111
128
|
return null
|
|
112
129
|
})
|
|
113
130
|
|
|
131
|
+
function checkFullscreen() {
|
|
132
|
+
isFullscreen.value = !!document.fullscreenElement;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
function onFullscreenChange() {
|
|
136
|
+
checkFullscreen();
|
|
137
|
+
emit('video-fullscreen-change', document.fullscreenElement)
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
|
|
114
141
|
function updateCurrentTime() {
|
|
115
142
|
videoCursor.value = video.value.currentTime;
|
|
116
143
|
}
|
|
117
144
|
|
|
145
|
+
function toggleTranscript() {
|
|
146
|
+
props.showTranscriptBlock = !props.showTranscriptBlock
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function seekVideo(time) {
|
|
150
|
+
video.value.currentTime = time;
|
|
151
|
+
video.play()
|
|
152
|
+
}
|
|
153
|
+
|
|
118
154
|
function prepareVideoPlayer() {
|
|
119
155
|
let hls = new Hls()
|
|
120
156
|
let stream = props.link
|
|
@@ -162,10 +198,15 @@ function prepareVideoPlayer() {
|
|
|
162
198
|
|
|
163
199
|
function pause() {
|
|
164
200
|
const currentTime = video?.value?.currentTime || 0
|
|
165
|
-
|
|
166
201
|
emit('pause', currentTime)
|
|
167
202
|
}
|
|
168
203
|
|
|
204
|
+
function onVideoEnd() {
|
|
205
|
+
const currentTime = video?.value?.currentTime || 0
|
|
206
|
+
pause()
|
|
207
|
+
emit('video-ended', { currentTime: currentTime, video });
|
|
208
|
+
}
|
|
209
|
+
|
|
169
210
|
function changeSpeed(e) {
|
|
170
211
|
if (e.key === 'w' && video && video.value) {
|
|
171
212
|
video.value.playbackRate = video.value.playbackRate + 0.25
|
|
@@ -186,17 +227,26 @@ function changeSpeed(e) {
|
|
|
186
227
|
position: absolute;
|
|
187
228
|
left: 50%;
|
|
188
229
|
width: auto;
|
|
189
|
-
max-width:
|
|
230
|
+
max-width: 95%;
|
|
190
231
|
text-align: center;
|
|
191
232
|
background: rgba(0, 0, 0, 0.7);
|
|
233
|
+
border-radius: 6px;
|
|
234
|
+
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
|
|
235
|
+
transform: translate(-50%) translateY(calc(-100% - 60px));
|
|
236
|
+
}
|
|
237
|
+
.custom-subtitles .subtitle-text {
|
|
192
238
|
color: white;
|
|
193
|
-
font-size:
|
|
239
|
+
font-size: 14px;
|
|
194
240
|
font-family: Arial, sans-serif;
|
|
195
241
|
line-height: 1.5;
|
|
196
|
-
padding: 10px
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
242
|
+
padding: 8px 10px;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.video-player-theme-container, .hls-player {
|
|
246
|
+
width: 100%;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.video-container {
|
|
250
|
+
position: relative;
|
|
201
251
|
}
|
|
202
252
|
</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,23 @@ 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
|
})
|
|
50
62
|
|
|
51
63
|
function pause(currentTime) {
|
|
52
64
|
emit('pause', currentTime)
|
|
53
65
|
}
|
|
66
|
+
function onVideoFullScreenChange(data) {
|
|
67
|
+
emit('video-fullscreen-change', data)
|
|
68
|
+
}
|
|
69
|
+
function onVideoEnd(data) {
|
|
70
|
+
emit('video-ended', data);
|
|
71
|
+
}
|
|
54
72
|
</script>
|