@grfzhl/vue-hls-player 1.1.22 → 1.1.24
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 +47 -39
- package/dist/VideoPlayer/BasePlayer.vue +323 -321
- package/dist/VideoPlayer/VDefaultVideoPlayer.vue +10 -1
- package/dist/VideoPlayer/index.vue +5 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -207,71 +207,79 @@ At the moment the following attribute are supported:
|
|
|
207
207
|
```
|
|
208
208
|
|
|
209
209
|
### Last release:
|
|
210
|
+
v1.1.24
|
|
211
|
+
- Add user-initiated language-changed emit.
|
|
212
|
+
- Fix unwanted language re-sync on player init.
|
|
213
|
+
- Improve audio/subtitle switch stability.
|
|
214
|
+
v1.1.23
|
|
215
|
+
- Fix missing property for subtitles in vue definition
|
|
216
|
+
- Clean up code
|
|
217
|
+
- Set mutation observer to body and the video element itself - whatever loads first
|
|
210
218
|
v1.1.22
|
|
211
|
-
- Only show language selection if subtitles or audio options are > 1
|
|
219
|
+
- Only show language selection if subtitles or audio options are > 1
|
|
212
220
|
v1.1.21
|
|
213
|
-
- Added more tolerant processing for .txt transcripts to allow empty lines as narrators
|
|
221
|
+
- Added more tolerant processing for .txt transcripts to allow empty lines as narrators
|
|
214
222
|
v1.1.20
|
|
215
|
-
- Stability and UI style improvements for Language switch
|
|
223
|
+
- Stability and UI style improvements for Language switch
|
|
216
224
|
v1.1.19
|
|
217
|
-
- Switcher supports both single and multi-language HLS sources
|
|
218
|
-
- Automatically syncs subtitle language with selected audio track
|
|
219
|
-
- Works with videos that include or omit subtitles
|
|
220
|
-
- Includes native cuechange event handling for subtitle updates
|
|
221
|
-
- Removed the default media-captions (CC) button from the player
|
|
225
|
+
- Switcher supports both single and multi-language HLS sources
|
|
226
|
+
- Automatically syncs subtitle language with selected audio track
|
|
227
|
+
- Works with videos that include or omit subtitles
|
|
228
|
+
- Includes native cuechange event handling for subtitle updates
|
|
229
|
+
- Removed the default media-captions (CC) button from the player
|
|
222
230
|
v1.1.18
|
|
223
|
-
- Added new slot `between-video-and-transcript` to `BasePlayer.vue`, `VDefaultVideoPlayer.vue` and `index.vue`
|
|
231
|
+
- Added new slot `between-video-and-transcript` to `BasePlayer.vue`, `VDefaultVideoPlayer.vue` and `index.vue`
|
|
224
232
|
to allow injection of custom UI between video and transcript.
|
|
225
|
-
- Introduced a continuous **Frame Pointer Loop** that emits a `pointer-update` event with the current playback time and calculated frame number (30 fps) for real-time frame tracking.
|
|
233
|
+
- Introduced a continuous **Frame Pointer Loop** that emits a `pointer-update` event with the current playback time and calculated frame number (30 fps) for real-time frame tracking.
|
|
226
234
|
v1.1.17
|
|
227
|
-
- Keep query params for transcription when getting from .vtt file to .txt
|
|
235
|
+
- Keep query params for transcription when getting from .vtt file to .txt
|
|
228
236
|
v1.1.16
|
|
229
|
-
- Add the options prop to both index.vue and VpDefaultVideoPlayer.vue to pass fullscreen label settings to the BasePlayer component.
|
|
230
|
-
- Update fullscreen toggle logic: adjust aria-label, add or remove mediaIsFullscreen attribute, and safely access media-tooltip via shadowRoot to ensure proper icon and tooltip state handling.
|
|
237
|
+
- Add the options prop to both index.vue and VpDefaultVideoPlayer.vue to pass fullscreen label settings to the BasePlayer component.
|
|
238
|
+
- Update fullscreen toggle logic: adjust aria-label, add or remove mediaIsFullscreen attribute, and safely access media-tooltip via shadowRoot to ensure proper icon and tooltip state handling.
|
|
231
239
|
v1.1.13 - v1.1.15
|
|
232
|
-
- Update the hls.js package
|
|
233
|
-
- Fixes
|
|
240
|
+
- Update the hls.js package
|
|
241
|
+
- Fixes
|
|
234
242
|
|
|
235
243
|
v1.1.12
|
|
236
|
-
- added component property to make it easier adding headers (like Authorization header into every hls request)
|
|
244
|
+
- added component property to make it easier adding headers (like Authorization header into every hls request)
|
|
237
245
|
|
|
238
246
|
v1.0.14 - v1.1.11
|
|
239
|
-
- Fixes
|
|
240
|
-
- iOS specific improvements
|
|
241
|
-
- New Options to customize the component
|
|
242
|
-
- Fix problem with not updating transcript highlighting
|
|
243
|
-
- Extending overloaded functions for fullscreen mode (WIP)
|
|
247
|
+
- Fixes
|
|
248
|
+
- iOS specific improvements
|
|
249
|
+
- New Options to customize the component
|
|
250
|
+
- Fix problem with not updating transcript highlighting
|
|
251
|
+
- Extending overloaded functions for fullscreen mode (WIP)
|
|
244
252
|
|
|
245
253
|
v1.0.9 - v1.0.14
|
|
246
|
-
- Fixes
|
|
247
|
-
- Small styling improvements
|
|
254
|
+
- Fixes
|
|
255
|
+
- Small styling improvements
|
|
248
256
|
|
|
249
257
|
v1.0.9
|
|
250
|
-
- Fix sizes in fullscreen mode for video
|
|
251
|
-
- Hide transcript block completely when hidden
|
|
258
|
+
- Fix sizes in fullscreen mode for video
|
|
259
|
+
- Hide transcript block completely when hidden
|
|
252
260
|
|
|
253
261
|
v1.0.8
|
|
254
|
-
- Add slots to inject own elements nearby video element
|
|
255
|
-
- Add prop for autoplay video
|
|
262
|
+
- Add slots to inject own elements nearby video element
|
|
263
|
+
- Add prop for autoplay video
|
|
256
264
|
|
|
257
265
|
v1.0.7
|
|
258
|
-
- Add function to handle own logic for fullscreen
|
|
266
|
+
- Add function to handle own logic for fullscreen
|
|
259
267
|
|
|
260
268
|
v1.0.6
|
|
261
|
-
- Small fixes
|
|
262
|
-
- Remove debug log
|
|
269
|
+
- Small fixes
|
|
270
|
+
- Remove debug log
|
|
263
271
|
|
|
264
272
|
v1.0.5
|
|
265
|
-
- Load transcriptions additionally to subtitles
|
|
266
|
-
- Add styled transcription block for better readability
|
|
267
|
-
- Improve interaction and dynamic params
|
|
273
|
+
- Load transcriptions additionally to subtitles
|
|
274
|
+
- Add styled transcription block for better readability
|
|
275
|
+
- Improve interaction and dynamic params
|
|
268
276
|
|
|
269
277
|
v1.0.4
|
|
270
|
-
- Make subtitles dynamic
|
|
271
|
-
- Add new switch to disable the subtitle block
|
|
272
|
-
- Fix some minor issues
|
|
278
|
+
- Make subtitles dynamic
|
|
279
|
+
- Add new switch to disable the subtitle block
|
|
280
|
+
- Fix some minor issues
|
|
273
281
|
|
|
274
282
|
v1.0.3
|
|
275
|
-
- Removed controls in favour of themable overlay by `player.style`.
|
|
276
|
-
- Updated hls library
|
|
277
|
-
- Added styled caption overlays. Added separate container to show all captions.
|
|
283
|
+
- Removed controls in favour of themable overlay by `player.style`.
|
|
284
|
+
- Updated hls library
|
|
285
|
+
- Added styled caption overlays. Added separate container to show all captions.
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
<slot name="between-video-and-transcript"></slot>
|
|
72
72
|
<slot name="before-transcripts"></slot>
|
|
73
73
|
<SubtitleBlock
|
|
74
|
-
:key="currentLang"
|
|
74
|
+
:key="`${currentLang}-${currentSubtitleLang}`"
|
|
75
75
|
ref="transcriptRef"
|
|
76
76
|
:subtitle="currentSubtitle"
|
|
77
77
|
:cursor="videoCursor"
|
|
@@ -167,7 +167,7 @@ const props = defineProps({
|
|
|
167
167
|
}
|
|
168
168
|
})
|
|
169
169
|
|
|
170
|
-
const emit = defineEmits(['pause', 'video-ended', 'video-fullscreen-change', 'pointer-update'])
|
|
170
|
+
const emit = defineEmits(['pause', 'video-ended', 'video-fullscreen-change', 'pointer-update', 'language-changed'])
|
|
171
171
|
const video = ref(null)
|
|
172
172
|
const subtitlesContainer = ref(null)
|
|
173
173
|
const currentSubtitleLang = ref(null)
|
|
@@ -179,12 +179,37 @@ const initialPlayButton = ref(false);
|
|
|
179
179
|
const hideInitialPlayButton = ref(false);
|
|
180
180
|
const transcriptRef = ref(null);
|
|
181
181
|
const previewImageLink = toRef(props, 'previewImageLink');
|
|
182
|
+
const subtitles = toRef(props, 'subtitles');
|
|
182
183
|
const link = toRef(props, 'link');
|
|
183
184
|
let currentTime = 0
|
|
184
185
|
let hls = null
|
|
185
186
|
let buttonElement = null
|
|
186
187
|
// --- lang switcher ---
|
|
187
188
|
const currentLang = ref(props.defaultLang || 'en')
|
|
189
|
+
const isUserInitiatedLangChange = ref(false)
|
|
190
|
+
|
|
191
|
+
let initialLoad = true;
|
|
192
|
+
|
|
193
|
+
watch(
|
|
194
|
+
() => props.defaultLang,
|
|
195
|
+
(newLang, oldLang) => {
|
|
196
|
+
if (initialLoad) {
|
|
197
|
+
initialLoad = false;
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (newLang && newLang !== oldLang && newLang !== currentLang.value) {
|
|
202
|
+
const hasSub = props.subtitles?.find(s => s.lang === newLang);
|
|
203
|
+
if (hasSub) {
|
|
204
|
+
currentSubtitleLang.value = newLang;
|
|
205
|
+
Array.from(video.value?.textTracks || []).forEach(track => {
|
|
206
|
+
const tLang = (track.language || track.srclang || '').toLowerCase();
|
|
207
|
+
track.mode = tLang === newLang.toLowerCase() ? 'showing' : 'disabled';
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
)
|
|
188
213
|
// --- Remember and restore last subtitle language ---
|
|
189
214
|
async function selectLang(lang) {
|
|
190
215
|
if (!video.value || !hls) return;
|
|
@@ -224,14 +249,19 @@ async function selectLang(lang) {
|
|
|
224
249
|
// Matching subtitles for matchLang
|
|
225
250
|
Array.from(video.value?.textTracks || []).forEach(track => {
|
|
226
251
|
const tLang = (track.language || track.srclang || '').toLowerCase();
|
|
227
|
-
console.log('[LangSwitch] Track', tLang, 'current mode:', track.mode);
|
|
228
252
|
track.mode = tLang === matchLang ? 'showing' : 'disabled';
|
|
229
|
-
console.log('[LangSwitch] Track', tLang, '→ new mode:', track.mode);
|
|
230
253
|
});
|
|
231
254
|
|
|
232
255
|
currentSubtitleLang.value = lang;
|
|
233
|
-
|
|
256
|
+
// Emit ONLY if user initiated the change
|
|
257
|
+
setTimeout(() => {
|
|
258
|
+
if (isUserInitiatedLangChange.value) {
|
|
259
|
+
emit('language-changed', lang);
|
|
260
|
+
isUserInitiatedLangChange.value = false; // Reset flag
|
|
261
|
+
}
|
|
262
|
+
}, 100);
|
|
234
263
|
});
|
|
264
|
+
// Attach HLS
|
|
235
265
|
await hls.loadSource(newSource.file_url);
|
|
236
266
|
video.value.muted = true;
|
|
237
267
|
} catch (err) {
|
|
@@ -254,7 +284,6 @@ function updateLangMenuState() {
|
|
|
254
284
|
const langElement = li.querySelector('span[data-lang]')
|
|
255
285
|
const liLang = langElement?.dataset?.lang;
|
|
256
286
|
// const liLang = li.textContent.trim().toLowerCase();
|
|
257
|
-
console.log("-- audio li lang", currentLang.value, liLang)
|
|
258
287
|
li.classList.toggle('active', liLang === currentLang.value.toLowerCase());
|
|
259
288
|
});
|
|
260
289
|
|
|
@@ -272,26 +301,6 @@ function updateLangMenuState() {
|
|
|
272
301
|
|
|
273
302
|
watch(currentSubtitleLang, () => updateLangMenuState());
|
|
274
303
|
|
|
275
|
-
/* function selectLang(lang) {
|
|
276
|
-
currentLang.value = lang;
|
|
277
|
-
showLangMenu.value = false;
|
|
278
|
-
|
|
279
|
-
const newSource = props.multiLangSources.find(s => s.lang === lang);
|
|
280
|
-
if (newSource) {
|
|
281
|
-
hls.stopLoad();
|
|
282
|
-
hls.detachMedia();
|
|
283
|
-
hls.loadSource(newSource.file_url);
|
|
284
|
-
hls.attachMedia(video.value);
|
|
285
|
-
hls.startLoad();
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// Subtitle sync
|
|
289
|
-
const textTracks = video.value.textTracks;
|
|
290
|
-
for (const track of textTracks) {
|
|
291
|
-
track.mode = track.language === lang ? 'showing' : 'disabled';
|
|
292
|
-
}
|
|
293
|
-
} */
|
|
294
|
-
|
|
295
304
|
// --- Frame Pointer Loop ---
|
|
296
305
|
let rafId = null
|
|
297
306
|
const FPS = 30
|
|
@@ -344,18 +353,20 @@ const mutedAttr = computed(() => {
|
|
|
344
353
|
return (props.autoplay || props.isMuted);
|
|
345
354
|
})
|
|
346
355
|
|
|
356
|
+
|
|
347
357
|
const currentSubtitle = computed(() => {
|
|
348
|
-
if(props.subtitles)
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
return subt.lang === "en"
|
|
354
|
-
}
|
|
355
|
-
})
|
|
356
|
-
return current.length ? current[0] : null
|
|
358
|
+
if (!props.subtitles?.length) return null;
|
|
359
|
+
|
|
360
|
+
if (currentSubtitleLang.value) {
|
|
361
|
+
const match = props.subtitles.find(s => s.lang === currentSubtitleLang.value);
|
|
362
|
+
if (match) return match;
|
|
357
363
|
}
|
|
358
|
-
|
|
364
|
+
|
|
365
|
+
// Fallback
|
|
366
|
+
const defaultMatch = props.subtitles.find(s => s.lang === props.defaultLang);
|
|
367
|
+
if (defaultMatch) return defaultMatch;
|
|
368
|
+
|
|
369
|
+
return props.subtitles[0];
|
|
359
370
|
})
|
|
360
371
|
|
|
361
372
|
watch(() => props.autoplay, (a) => {
|
|
@@ -537,7 +548,6 @@ function prepareVideoPlayer(link) {
|
|
|
537
548
|
|
|
538
549
|
// Preparing video player with link: ${link}
|
|
539
550
|
hls = new Hls(hlsConfig);
|
|
540
|
-
|
|
541
551
|
// Attach HLS
|
|
542
552
|
hls.loadSource(link);
|
|
543
553
|
hls.attachMedia(video.value);
|
|
@@ -580,14 +590,16 @@ function prepareVideoPlayer(link) {
|
|
|
580
590
|
// Initialize subtitles
|
|
581
591
|
if (props.subtitles?.length > 0) {
|
|
582
592
|
const defaultSub = props.subtitles.find(s => s.lang === props.defaultLang);
|
|
583
|
-
currentSubtitleLang.value = defaultSub ?
|
|
593
|
+
currentSubtitleLang.value = defaultSub ? props.defaultLang : props.subtitles[0].lang;
|
|
584
594
|
Array.from(video.value?.textTracks || []).forEach(track => {
|
|
585
595
|
const tLang = (track.language || track.srclang || '').toLowerCase();
|
|
586
|
-
|
|
587
|
-
track.mode =
|
|
596
|
+
const shouldShow = tLang === currentSubtitleLang.value.toLowerCase();
|
|
597
|
+
track.mode = shouldShow ? 'showing' : 'disabled';
|
|
598
|
+
// console.log('[SubtitleInit] Track found:', tLang, '->', shouldShow ? 'showing' : 'disabled');
|
|
588
599
|
});
|
|
589
600
|
}
|
|
590
601
|
|
|
602
|
+
selectLang(props.defaultLang);
|
|
591
603
|
// HLS attached to <video>
|
|
592
604
|
hls.recoverMediaError();
|
|
593
605
|
|
|
@@ -639,284 +651,284 @@ function initVideo() {
|
|
|
639
651
|
* overwrite player.style video fullscreen button
|
|
640
652
|
* to inject own fullscreen logic
|
|
641
653
|
*/
|
|
642
|
-
const observer = new MutationObserver(
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
if (fullscreenButton) {
|
|
652
|
-
fullscreenButton.handleClick = async (event) => {
|
|
653
|
-
event.preventDefault();
|
|
654
|
-
event.stopPropagation();
|
|
655
|
-
event.stopImmediatePropagation();
|
|
656
|
-
startFullscreen();
|
|
657
|
-
}
|
|
658
|
-
// --- Remove default CC button ---
|
|
659
|
-
const ccButton = mediaTheme.shadowRoot.querySelector('media-captions-button');
|
|
660
|
-
if (ccButton) {
|
|
661
|
-
ccButton.remove();
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
// --- Amazon Prime Style Language Switcher ---
|
|
665
|
-
if (controlBar && !controlBar.querySelector('.lang-switcher')
|
|
666
|
-
&& (props.multiLangSources.length > 1 || props.subtitles.length > 1)) {
|
|
667
|
-
const langDiv = document.createElement('div');
|
|
668
|
-
langDiv.className = 'lang-switcher';
|
|
669
|
-
langDiv.innerHTML = `
|
|
670
|
-
<style>
|
|
671
|
-
.lang-switcher {
|
|
672
|
-
position: relative;
|
|
673
|
-
display: flex;
|
|
674
|
-
align-items: center;
|
|
675
|
-
justify-content: center;
|
|
676
|
-
margin-left: var(--media-control-spacing, 6px);
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
.lang-switcher button {
|
|
680
|
-
all: unset;
|
|
681
|
-
display: flex;
|
|
682
|
-
align-items: center;
|
|
683
|
-
justify-content: center;
|
|
684
|
-
cursor: pointer;
|
|
685
|
-
height: var(--media-button-height, 32px);
|
|
686
|
-
border-radius: 25%;
|
|
687
|
-
min-width: var(--media-button-height, 32px);
|
|
688
|
-
padding: var(--media-button-padding, 0 5px);
|
|
689
|
-
background: transparent;
|
|
690
|
-
transition: background 0.15s ease, transform 0.1s ease;
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
.lang-switcher button svg {
|
|
694
|
-
width: var(--media-icon-size, 24px);
|
|
695
|
-
height: var(--media-icon-size, 24px);
|
|
696
|
-
color: var(--media-icon-color, white);
|
|
697
|
-
stroke: currentColor;
|
|
698
|
-
opacity: var(--media-icon-opacity, 0.9);
|
|
699
|
-
pointer-events: none;
|
|
700
|
-
transition: opacity 0.15s ease;
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
.lang-switcher button:hover svg {
|
|
704
|
-
transform: scale(1.1);
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
.lang-switcher button:hover {
|
|
708
|
-
opacity: var(--media-icon-opacity-hover, 1);
|
|
709
|
-
background: var(--media-control-hover-background, rgba(50 50 70 / .7));
|
|
710
|
-
transition: backdrop-filter 0.3s, -webkit-backdrop-filter 0.3s;
|
|
711
|
-
box-shadow: rgba(0, 0, 0, 0.3) 0px 0px 5px;
|
|
712
|
-
backdrop-filter: blur(10px) invert(15%) brightness(80%) opacity(1);
|
|
713
|
-
padding: 5px;
|
|
714
|
-
color: var(--media-text-color, var(--media-primary-color, rgb(238 238 238)));
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
.lang-btn[title]:hover::after {
|
|
718
|
-
content: attr(title);
|
|
719
|
-
position: fixed;
|
|
720
|
-
bottom: 122%;
|
|
721
|
-
right: -100%;
|
|
722
|
-
white-space: nowrap;
|
|
723
|
-
background: var(--media-tooltip-background, rgba(0,0,0,0.2));
|
|
724
|
-
color: var(--media-tooltip-color, #fff);
|
|
725
|
-
border-radius: 4px;
|
|
726
|
-
padding: 4px 15px;
|
|
727
|
-
font-size: var(--media-font-size, 12px);
|
|
728
|
-
box-shadow: 0 2px 8px rgba(0,0,0,0.4);
|
|
729
|
-
line-height: calc(1.2 * var(--base));
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
.lang-menu {
|
|
733
|
-
position: absolute;
|
|
734
|
-
bottom: calc(var(--media-button-height, 32px) * 1.7);
|
|
735
|
-
right: -48px;
|
|
736
|
-
background: var(--media-control-bar-background, rgba(20,20,20,0.4));
|
|
737
|
-
backdrop-filter: blur(12px);
|
|
738
|
-
border-radius: 12px;
|
|
739
|
-
box-shadow: rgba(0, 0, 0, 0.3) 0px 0px 5px;
|
|
740
|
-
padding: 10px;
|
|
741
|
-
min-width: 240px;
|
|
742
|
-
display: none;
|
|
743
|
-
animation: fadeUp 0.15s ease-out;
|
|
744
|
-
color: var(--media-text-color, #fff);
|
|
745
|
-
font-family: var(--media-font-family, system-ui, sans-serif);
|
|
746
|
-
font-size: var(--media-font-size, 13px);
|
|
747
|
-
z-index: 9999;
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
@keyframes fadeUp {
|
|
751
|
-
from { opacity: 0; transform: translateY(8px); }
|
|
752
|
-
to { opacity: 1; transform: translateY(0); }
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
.lang-columns {
|
|
756
|
-
display: flex;
|
|
757
|
-
justify-content: space-between;
|
|
758
|
-
gap: 1rem;
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
.lang-col {
|
|
762
|
-
flex: 1;
|
|
763
|
-
display: flex;
|
|
764
|
-
flex-direction: column;
|
|
765
|
-
margin: 0;
|
|
766
|
-
padding: 0;
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
.lang-col .title {
|
|
770
|
-
line-height: calc(1.2 * var(--base));
|
|
771
|
-
font-weight: 600;
|
|
772
|
-
font-size: calc(var(--media-font-size, 13px) - 1px);
|
|
773
|
-
margin-bottom: 4px;
|
|
774
|
-
border-bottom: 1px solid rgba(255,255,255,0.1);
|
|
775
|
-
padding-bottom: 2px;
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
.lang-col ul {
|
|
779
|
-
list-style: none;
|
|
780
|
-
margin: 0;
|
|
781
|
-
padding: 0;
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
.lang-col li {
|
|
785
|
-
display: flex;
|
|
786
|
-
align-items: center;
|
|
787
|
-
justify-content: space-between;
|
|
788
|
-
cursor: pointer;
|
|
789
|
-
border-radius: 4px;
|
|
790
|
-
padding: 2px 12px;
|
|
791
|
-
transition: background 0.15s ease, color 0.15s ease;
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
.lang-col li span {
|
|
795
|
-
line-height: calc(1.2 * var(--base));
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
.lang-col li:hover {
|
|
799
|
-
background: var(--media-control-hover-background, rgba(255,255,255,0.15));
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
.lang-col li.active {
|
|
803
|
-
font-weight: 500;
|
|
804
|
-
color: var(--media-accent-color, #fff);
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
.lang-col li .icon {
|
|
808
|
-
opacity: 0;
|
|
809
|
-
transition: opacity 0.15s ease, transform 0.15s ease;
|
|
810
|
-
transform: translateX(2px);
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
.lang-col li.active .icon {
|
|
814
|
-
opacity: 1;
|
|
815
|
-
transform: translateX(0);
|
|
816
|
-
}
|
|
817
|
-
</style>
|
|
818
|
-
|
|
819
|
-
<button title="Audio & Subtitles" class="lang-btn">
|
|
820
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 26 26" fill="none" stroke="currentColor" stroke-width="1.5"
|
|
821
|
-
stroke-linecap="round" stroke-linejoin="round">
|
|
822
|
-
<path d="m5 8 6 6"/>
|
|
823
|
-
<path d="m4 14 6-6 2-3"/>
|
|
824
|
-
<path d="M2 5h12"/>
|
|
825
|
-
<path d="M7 2h1"/>
|
|
826
|
-
<path d="m22 22-5-10-5 10"/>
|
|
827
|
-
<path d="M14 18h6"/>
|
|
828
|
-
</svg>
|
|
829
|
-
</button>
|
|
830
|
-
|
|
831
|
-
<div class="lang-menu">
|
|
832
|
-
<div class="lang-columns">
|
|
833
|
-
<ul class="lang-col audio-col"><li class="title">Audio</li></ul>
|
|
834
|
-
<ul class="lang-col sub-col"><li class="title">Subtitles</li></ul>
|
|
835
|
-
</div>
|
|
836
|
-
</div>
|
|
837
|
-
`;
|
|
838
|
-
|
|
839
|
-
const menu = langDiv.querySelector('.lang-menu');
|
|
840
|
-
const audioCol = menu.querySelector('.audio-col');
|
|
841
|
-
const subCol = menu.querySelector('.sub-col');
|
|
842
|
-
const button = langDiv.querySelector('button');
|
|
843
|
-
menu.addEventListener('click', e => e.stopPropagation());
|
|
844
|
-
// Chrome Media Player style check icon
|
|
845
|
-
const renderIcon = () => `
|
|
846
|
-
<svg aria-hidden="true" viewBox="0 1 24 24" width="16" height="16" part="checked-indicator indicator">
|
|
847
|
-
<path fill="currentColor" d="m10 15.17 9.193-9.191 1.414 1.414-10.606 10.606-6.364-6.364 1.414-1.414 4.95 4.95Z"></path>
|
|
848
|
-
</svg>
|
|
849
|
-
`;
|
|
850
|
-
|
|
851
|
-
// Audio options
|
|
852
|
-
props.multiLangSources.forEach(src => {
|
|
853
|
-
const li = document.createElement('li');
|
|
854
|
-
li.innerHTML = `
|
|
855
|
-
<span data-lang="${ src.lang }">${src.label || src.lang.toUpperCase()}</span>
|
|
856
|
-
<span class="icon">${renderIcon()}</span>
|
|
857
|
-
`;
|
|
858
|
-
if (src.lang === currentLang.value) li.classList.add('active');
|
|
859
|
-
li.addEventListener('click', () => {
|
|
860
|
-
audioCol.querySelectorAll('li').forEach(el => el.classList.remove('active'));
|
|
861
|
-
li.classList.add('active');
|
|
862
|
-
selectLang(src.lang);
|
|
863
|
-
menu.style.display = 'none';
|
|
864
|
-
});
|
|
865
|
-
audioCol.appendChild(li);
|
|
866
|
-
});
|
|
867
|
-
|
|
868
|
-
props.subtitles.forEach(sub => {
|
|
869
|
-
const li = document.createElement('li');
|
|
870
|
-
li.innerHTML = `
|
|
871
|
-
<span data-lang="${ sub.lang }">${sub.label || sub.lang.toUpperCase()}</span>
|
|
872
|
-
<span class="icon">${renderIcon()}</span>
|
|
873
|
-
`;
|
|
874
|
-
|
|
875
|
-
if (sub.lang === currentSubtitleLang.value) li.classList.add('active');
|
|
876
|
-
li.addEventListener('click', () => {
|
|
877
|
-
Array.from(video.value?.textTracks || []).forEach(track => {
|
|
878
|
-
const tLang = (track.language || track.srclang || '').toLowerCase();
|
|
879
|
-
track.mode = tLang === sub.lang.toLowerCase() ? 'showing' : 'disabled';
|
|
880
|
-
});
|
|
881
|
-
subCol.querySelectorAll('li').forEach(el => el.classList.remove('active'));
|
|
882
|
-
li.classList.add('active');
|
|
883
|
-
menu.style.display = 'none';
|
|
884
|
-
currentSubtitleLang.value = sub.lang;
|
|
885
|
-
});
|
|
886
|
-
subCol.appendChild(li);
|
|
887
|
-
});
|
|
654
|
+
const observer = new MutationObserver(mutationObserver)
|
|
655
|
+
// --- Start pointer-update loop ---
|
|
656
|
+
if (!rafId) {
|
|
657
|
+
emitPointerUpdate()
|
|
658
|
+
}
|
|
659
|
+
observer.observe(document.querySelector('.video-player-theme-container'), { childList: true, subtree: true });
|
|
660
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
661
|
+
}
|
|
662
|
+
}
|
|
888
663
|
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
664
|
+
const mutationObserver = (mutationsList, observer) => {
|
|
665
|
+
const mediaTheme = document.querySelector('.video-player-theme-container');
|
|
666
|
+
if (mediaTheme && mediaTheme.shadowRoot) {
|
|
667
|
+
// controllbar
|
|
668
|
+
const controlBar = mediaTheme.shadowRoot.querySelector('media-control-bar');
|
|
669
|
+
const fullscreenButton = mediaTheme.shadowRoot.querySelector('media-fullscreen-button');
|
|
670
|
+
buttonElement = fullscreenButton
|
|
671
|
+
const playbackRateButton = mediaTheme.shadowRoot.querySelector('media-playback-rate-menu');
|
|
672
|
+
playbackRateButton.setAttribute('rates', '0.25 0.5 0.75 1 1.5 2 3');
|
|
673
|
+
if (fullscreenButton) {
|
|
674
|
+
fullscreenButton.handleClick = async (event) => {
|
|
675
|
+
event.preventDefault();
|
|
676
|
+
event.stopPropagation();
|
|
677
|
+
event.stopImmediatePropagation();
|
|
678
|
+
startFullscreen();
|
|
679
|
+
}
|
|
680
|
+
// --- Remove default CC button ---
|
|
681
|
+
const ccButton = mediaTheme.shadowRoot.querySelector('media-captions-button');
|
|
682
|
+
if (ccButton) {
|
|
683
|
+
ccButton.remove();
|
|
684
|
+
}
|
|
685
|
+
// --- Amazon Prime Style Language Switcher ---
|
|
686
|
+
if (controlBar && !controlBar.querySelector('.lang-switcher')
|
|
687
|
+
&& (props.multiLangSources.length > 1 || props.subtitles.length > 1)) {
|
|
688
|
+
const langDiv = document.createElement('div');
|
|
689
|
+
langDiv.className = 'lang-switcher';
|
|
690
|
+
langDiv.innerHTML = `
|
|
691
|
+
<style>
|
|
692
|
+
.lang-switcher {
|
|
693
|
+
position: relative;
|
|
694
|
+
display: flex;
|
|
695
|
+
align-items: center;
|
|
696
|
+
justify-content: center;
|
|
697
|
+
margin-left: var(--media-control-spacing, 6px);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
.lang-switcher button {
|
|
701
|
+
all: unset;
|
|
702
|
+
display: flex;
|
|
703
|
+
align-items: center;
|
|
704
|
+
justify-content: center;
|
|
705
|
+
cursor: pointer;
|
|
706
|
+
height: var(--media-button-height, 32px);
|
|
707
|
+
border-radius: 25%;
|
|
708
|
+
min-width: var(--media-button-height, 32px);
|
|
709
|
+
padding: var(--media-button-padding, 0 5px);
|
|
710
|
+
background: transparent;
|
|
711
|
+
transition: background 0.15s ease, transform 0.1s ease;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
.lang-switcher button svg {
|
|
715
|
+
width: var(--media-icon-size, 24px);
|
|
716
|
+
height: var(--media-icon-size, 24px);
|
|
717
|
+
color: var(--media-icon-color, white);
|
|
718
|
+
stroke: currentColor;
|
|
719
|
+
opacity: var(--media-icon-opacity, 0.9);
|
|
720
|
+
pointer-events: none;
|
|
721
|
+
transition: opacity 0.15s ease;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
.lang-switcher button:hover svg {
|
|
725
|
+
transform: scale(1.1);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
.lang-switcher button:hover {
|
|
729
|
+
opacity: var(--media-icon-opacity-hover, 1);
|
|
730
|
+
background: var(--media-control-hover-background, rgba(50 50 70 / .7));
|
|
731
|
+
transition: backdrop-filter 0.3s, -webkit-backdrop-filter 0.3s;
|
|
732
|
+
box-shadow: rgba(0, 0, 0, 0.3) 0px 0px 5px;
|
|
733
|
+
backdrop-filter: blur(10px) invert(15%) brightness(80%) opacity(1);
|
|
734
|
+
padding: 5px;
|
|
735
|
+
color: var(--media-text-color, var(--media-primary-color, rgb(238 238 238)));
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
.lang-btn[title]:hover::after {
|
|
739
|
+
content: attr(title);
|
|
740
|
+
position: fixed;
|
|
741
|
+
bottom: 122%;
|
|
742
|
+
right: -100%;
|
|
743
|
+
white-space: nowrap;
|
|
744
|
+
background: var(--media-tooltip-background, rgba(0,0,0,0.2));
|
|
745
|
+
color: var(--media-tooltip-color, #fff);
|
|
746
|
+
border-radius: 4px;
|
|
747
|
+
padding: 4px 15px;
|
|
748
|
+
font-size: var(--media-font-size, 12px);
|
|
749
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.4);
|
|
750
|
+
line-height: calc(1.2 * var(--base));
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
.lang-menu {
|
|
754
|
+
position: absolute;
|
|
755
|
+
bottom: calc(var(--media-button-height, 32px) * 1.7);
|
|
756
|
+
right: -48px;
|
|
757
|
+
background: var(--media-control-bar-background, rgba(20,20,20,0.4));
|
|
758
|
+
backdrop-filter: blur(12px);
|
|
759
|
+
border-radius: 12px;
|
|
760
|
+
box-shadow: rgba(0, 0, 0, 0.3) 0px 0px 5px;
|
|
761
|
+
padding: 10px;
|
|
762
|
+
min-width: 240px;
|
|
763
|
+
display: none;
|
|
764
|
+
animation: fadeUp 0.15s ease-out;
|
|
765
|
+
color: var(--media-text-color, #fff);
|
|
766
|
+
font-family: var(--media-font-family, system-ui, sans-serif);
|
|
767
|
+
font-size: var(--media-font-size, 13px);
|
|
768
|
+
z-index: 9999;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
@keyframes fadeUp {
|
|
772
|
+
from { opacity: 0; transform: translateY(8px); }
|
|
773
|
+
to { opacity: 1; transform: translateY(0); }
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
.lang-columns {
|
|
777
|
+
display: flex;
|
|
778
|
+
justify-content: space-between;
|
|
779
|
+
gap: 1rem;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
.lang-col {
|
|
783
|
+
flex: 1;
|
|
784
|
+
display: flex;
|
|
785
|
+
flex-direction: column;
|
|
786
|
+
margin: 0;
|
|
787
|
+
padding: 0;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
.lang-col .title {
|
|
791
|
+
line-height: calc(1.2 * var(--base));
|
|
792
|
+
font-weight: 600;
|
|
793
|
+
font-size: calc(var(--media-font-size, 13px) - 1px);
|
|
794
|
+
margin-bottom: 4px;
|
|
795
|
+
border-bottom: 1px solid rgba(255,255,255,0.1);
|
|
796
|
+
padding-bottom: 2px;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
.lang-col ul {
|
|
800
|
+
list-style: none;
|
|
801
|
+
margin: 0;
|
|
802
|
+
padding: 0;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
.lang-col li {
|
|
806
|
+
display: flex;
|
|
807
|
+
align-items: center;
|
|
808
|
+
justify-content: space-between;
|
|
809
|
+
cursor: pointer;
|
|
810
|
+
border-radius: 4px;
|
|
811
|
+
padding: 2px 12px;
|
|
812
|
+
transition: background 0.15s ease, color 0.15s ease;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
.lang-col li span {
|
|
816
|
+
line-height: calc(1.2 * var(--base));
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
.lang-col li:hover {
|
|
820
|
+
background: var(--media-control-hover-background, rgba(255,255,255,0.15));
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
.lang-col li.active {
|
|
824
|
+
font-weight: 500;
|
|
825
|
+
color: var(--media-accent-color, #fff);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
.lang-col li .icon {
|
|
829
|
+
opacity: 0;
|
|
830
|
+
transition: opacity 0.15s ease, transform 0.15s ease;
|
|
831
|
+
transform: translateX(2px);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
.lang-col li.active .icon {
|
|
835
|
+
opacity: 1;
|
|
836
|
+
transform: translateX(0);
|
|
837
|
+
}
|
|
838
|
+
</style>
|
|
839
|
+
|
|
840
|
+
<button title="Audio & Subtitles" class="lang-btn">
|
|
841
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 26 26" fill="none" stroke="currentColor" stroke-width="1.5"
|
|
842
|
+
stroke-linecap="round" stroke-linejoin="round">
|
|
843
|
+
<path d="m5 8 6 6"/>
|
|
844
|
+
<path d="m4 14 6-6 2-3"/>
|
|
845
|
+
<path d="M2 5h12"/>
|
|
846
|
+
<path d="M7 2h1"/>
|
|
847
|
+
<path d="m22 22-5-10-5 10"/>
|
|
848
|
+
<path d="M14 18h6"/>
|
|
849
|
+
</svg>
|
|
850
|
+
</button>
|
|
851
|
+
|
|
852
|
+
<div class="lang-menu">
|
|
853
|
+
<div class="lang-columns">
|
|
854
|
+
<ul class="lang-col audio-col"><li class="title">Audio</li></ul>
|
|
855
|
+
<ul class="lang-col sub-col"><li class="title">Subtitles</li></ul>
|
|
856
|
+
</div>
|
|
857
|
+
</div>
|
|
858
|
+
`;
|
|
859
|
+
const menu = langDiv.querySelector('.lang-menu');
|
|
860
|
+
const audioCol = menu.querySelector('.audio-col');
|
|
861
|
+
const subCol = menu.querySelector('.sub-col');
|
|
862
|
+
const button = langDiv.querySelector('button');
|
|
863
|
+
menu.addEventListener('click', e => e.stopPropagation());
|
|
864
|
+
// Chrome Media Player style check icon
|
|
865
|
+
const renderIcon = () => `
|
|
866
|
+
<svg aria-hidden="true" viewBox="0 1 24 24" width="16" height="16" part="checked-indicator indicator">
|
|
867
|
+
<path fill="currentColor" d="m10 15.17 9.193-9.191 1.414 1.414-10.606 10.606-6.364-6.364 1.414-1.414 4.95 4.95Z"></path>
|
|
868
|
+
</svg>
|
|
869
|
+
`;
|
|
870
|
+
|
|
871
|
+
// Audio options
|
|
872
|
+
props.multiLangSources.forEach(src => {
|
|
873
|
+
const li = document.createElement('li');
|
|
874
|
+
li.innerHTML = `
|
|
875
|
+
<span data-lang="${ src.lang }">${src.label || src.lang.toUpperCase()}</span>
|
|
876
|
+
<span class="icon">${renderIcon()}</span>
|
|
877
|
+
`;
|
|
878
|
+
if (src.lang === currentLang.value) li.classList.add('active');
|
|
879
|
+
li.addEventListener('click', () => {
|
|
880
|
+
audioCol.querySelectorAll('li').forEach(el => el.classList.remove('active'));
|
|
881
|
+
li.classList.add('active');
|
|
882
|
+
isUserInitiatedLangChange.value = true;
|
|
883
|
+
selectLang(src.lang);
|
|
884
|
+
menu.style.display = 'none';
|
|
885
|
+
});
|
|
886
|
+
audioCol.appendChild(li);
|
|
887
|
+
});
|
|
888
|
+
props.subtitles.forEach(sub => {
|
|
889
|
+
const li = document.createElement('li');
|
|
890
|
+
li.innerHTML = `
|
|
891
|
+
<span data-lang="${ sub.lang }">${sub.label || sub.lang.toUpperCase()}</span>
|
|
892
|
+
<span class="icon">${renderIcon()}</span>
|
|
893
|
+
`;
|
|
894
|
+
|
|
895
|
+
if (sub.lang === currentSubtitleLang.value) li.classList.add('active');
|
|
896
|
+
li.addEventListener('click', () => {
|
|
897
|
+
Array.from(video.value?.textTracks || []).forEach(track => {
|
|
898
|
+
const tLang = (track.language || track.srclang || '').toLowerCase();
|
|
899
|
+
track.mode = tLang === sub.lang.toLowerCase() ? 'showing' : 'disabled';
|
|
901
900
|
});
|
|
901
|
+
subCol.querySelectorAll('li').forEach(el => el.classList.remove('active'));
|
|
902
|
+
li.classList.add('active');
|
|
903
|
+
menu.style.display = 'none';
|
|
904
|
+
currentSubtitleLang.value = sub.lang;
|
|
905
|
+
});
|
|
906
|
+
subCol.appendChild(li);
|
|
907
|
+
});
|
|
902
908
|
|
|
903
|
-
|
|
909
|
+
button.addEventListener('click', e => {
|
|
910
|
+
e.stopPropagation();
|
|
911
|
+
const isOpen = menu.style.display === 'block';
|
|
912
|
+
menu.style.display = isOpen ? 'none' : 'block';
|
|
913
|
+
updateLangMenuState();
|
|
914
|
+
});
|
|
915
|
+
document.addEventListener('click', e => {
|
|
916
|
+
if (!menu.contains(e.target) && !button.contains(e.target)) {
|
|
917
|
+
menu.style.display = 'none';
|
|
918
|
+
button.setAttribute('aria-expanded', 'false');
|
|
919
|
+
updateLangMenuState();
|
|
904
920
|
}
|
|
921
|
+
});
|
|
905
922
|
|
|
906
|
-
|
|
907
|
-
} else {
|
|
908
|
-
console.error('Button not found in Shadow DOM!');
|
|
909
|
-
}
|
|
910
|
-
} else {
|
|
911
|
-
console.error('Shadow Root not found!');
|
|
923
|
+
controlBar.insertBefore(langDiv, fullscreenButton);
|
|
912
924
|
}
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
emitPointerUpdate()
|
|
925
|
+
|
|
926
|
+
observer.disconnect();
|
|
927
|
+
} else {
|
|
928
|
+
console.error('Button not found in Shadow DOM!');
|
|
918
929
|
}
|
|
919
|
-
|
|
930
|
+
} else {
|
|
931
|
+
console.error('Shadow Root not found!');
|
|
920
932
|
}
|
|
921
933
|
}
|
|
922
934
|
|
|
@@ -938,16 +950,6 @@ function changeSpeed(e) {
|
|
|
938
950
|
video.value.playbackRate = video.value.playbackRate - 0.25
|
|
939
951
|
}
|
|
940
952
|
}
|
|
941
|
-
defineExpose({ startFullscreen, getHls: () => hls, getVideo: () => video.value,
|
|
942
|
-
changeSource: (newLink) => {
|
|
943
|
-
if (!newLink) return;
|
|
944
|
-
hls.stopLoad();
|
|
945
|
-
hls.detachMedia();
|
|
946
|
-
hls.loadSource(newLink);
|
|
947
|
-
hls.attachMedia(video.value);
|
|
948
|
-
hls.startLoad();
|
|
949
|
-
}
|
|
950
|
-
});
|
|
951
953
|
|
|
952
954
|
</script>
|
|
953
955
|
<style>
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
:autoplay="autoplay"
|
|
10
10
|
:isControls="isControls"
|
|
11
11
|
:onVideoEnd="onVideoEnd"
|
|
12
|
+
:subtitles="subtitles"
|
|
12
13
|
:additionHeaders="additionHeaders"
|
|
13
14
|
:isFullscreen="isFullscreen"
|
|
14
15
|
:showTranscriptBlock="showTranscriptBlock"
|
|
@@ -16,6 +17,7 @@
|
|
|
16
17
|
@pause="pause"
|
|
17
18
|
@video-ended="onVideoEnd"
|
|
18
19
|
@video-fullscreen-change="onFullscreenChange"
|
|
20
|
+
@language-changed="onLanguageChanged"
|
|
19
21
|
v-model="videoElement"
|
|
20
22
|
ref="childRef"
|
|
21
23
|
>
|
|
@@ -31,7 +33,7 @@
|
|
|
31
33
|
import BasePlayer from './BasePlayer.vue'
|
|
32
34
|
import { ref, toRef } from 'vue'
|
|
33
35
|
|
|
34
|
-
const emit = defineEmits(['pause', 'video-ended', 'video-fullscreen-change'])
|
|
36
|
+
const emit = defineEmits(['pause', 'video-ended', 'video-fullscreen-change', 'language-changed'])
|
|
35
37
|
|
|
36
38
|
const videoElement = ref(null);
|
|
37
39
|
const childRef = ref(null)
|
|
@@ -88,6 +90,10 @@ const props = defineProps({
|
|
|
88
90
|
additionHeaders: {
|
|
89
91
|
type: Object,
|
|
90
92
|
default: {}
|
|
93
|
+
},
|
|
94
|
+
subtitles: {
|
|
95
|
+
type: Array,
|
|
96
|
+
default: []
|
|
91
97
|
}
|
|
92
98
|
})
|
|
93
99
|
|
|
@@ -109,6 +115,9 @@ function onFullscreenChange(data) {
|
|
|
109
115
|
emit('video-fullscreen-change', data);
|
|
110
116
|
}
|
|
111
117
|
|
|
118
|
+
function onLanguageChanged(data) {
|
|
119
|
+
emit('language-changed', data);
|
|
120
|
+
}
|
|
112
121
|
function startFullscreen() {
|
|
113
122
|
childRef.value.startFullscreen();
|
|
114
123
|
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
:isFullscreen="isFullscreen"
|
|
12
12
|
:link="link"
|
|
13
13
|
:progress="progress"
|
|
14
|
+
:subtitles="subtitles"
|
|
14
15
|
:isMuted="isMuted"
|
|
15
16
|
:autoplay="autoplay"
|
|
16
17
|
v-model="videoElement"
|
|
@@ -98,6 +99,10 @@ const props = defineProps({
|
|
|
98
99
|
additionHeaders: {
|
|
99
100
|
type: Object,
|
|
100
101
|
default: {}
|
|
102
|
+
},
|
|
103
|
+
subtitles: {
|
|
104
|
+
type: Array,
|
|
105
|
+
default: []
|
|
101
106
|
}
|
|
102
107
|
})
|
|
103
108
|
|