@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
|
|
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>
|
|
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', '
|
|
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:
|
|
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:
|
|
241
|
+
font-size: 14px;
|
|
173
242
|
font-family: Arial, sans-serif;
|
|
174
243
|
line-height: 1.5;
|
|
175
|
-
padding: 10px
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
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
|
-
|
|
24
|
-
type:
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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>
|