fix: video player did not support multi subs
Some checks failed
build-docker-containers / Validate Build Configuration (push) Has been cancelled
build-docker-containers / DockerHub README sync (push) Has been cancelled
build-docker-containers / Run Tests & Prepare Frontend (push) Has been cancelled
build-docker-containers / Build Container (amd64) (push) Has been cancelled
build-docker-containers / Build Container (arm64) (push) Has been cancelled
build-docker-containers / Publish multi-arch manifest (push) Has been cancelled

This commit is contained in:
arabcoders 2026-06-23 10:42:01 +03:00
parent e14387e399
commit 60b8e7c4a6
2 changed files with 91 additions and 39 deletions

View file

@ -94,6 +94,7 @@
:label="nativeSubtitleTrack.name || 'Subtitles'"
default
:src="uri(nativeSubtitleTrack.url)"
@load="handleNativeTrackLoad"
/>
Your browser does not support the video tag.
</video>
@ -170,16 +171,30 @@
@click="forceSwitchToHls"
/>
</UTooltip>
<UButton
<USelect
v-if="hasSubtitles"
v-model="subtitleSelectValue"
:items="subtitleSelectItems"
value-key="value"
label-key="label"
color="neutral"
variant="soft"
size="sm"
trailing-icon=""
:content="subtitleSelectContent"
:portal="helpPortal"
:ui="subtitleSelectUi"
class="opacity-65 transition-opacity hover:opacity-100 focus-visible:opacity-100"
:icon="subtitleEnabled ? 'i-lucide-captions' : 'i-lucide-captions-off'"
:aria-label="subtitleEnabled ? 'Disable subtitles' : 'Enable subtitles'"
@click="subtitleEnabled = !subtitleEnabled"
/>
:aria-label="subtitleButtonLabel"
>
<template #default>
<span class="sr-only">{{ subtitleButtonLabel }}</span>
<UIcon
:name="subtitleEnabled ? 'i-lucide-captions' : 'i-lucide-captions-off'"
class="size-4 shrink-0"
/>
</template>
</USelect>
<UButton
color="neutral"
variant="soft"
@ -429,9 +444,12 @@ const timeLabel = computed(() => {
return `${currentLabel} / ${durationLabel}`;
});
const {
subtitleTracks,
subtitleLoading,
subtitleLoadError,
subtitleEnabled,
selectedSubtitleTrack,
selectedSubtitleTrackId,
nativeSubtitleTrack,
usesAssSubtitleTrack,
hasSubtitles,
@ -445,6 +463,40 @@ const {
overlay: assOverlayElement,
});
const SUBTITLE_OFF_VALUE = '__off__';
const subtitleSelectContent = { align: 'end' as const } as const;
const subtitleSelectUi = {
base: 'size-8 min-h-8 min-w-8 justify-center rounded-md px-0',
value: 'flex items-center justify-center px-0',
trailing: 'hidden',
leading: 'hidden',
content: 'z-40 min-w-56 w-auto max-w-[min(24rem,calc(100vw-2rem))]',
} as const;
const subtitleSelectItems = computed(() => [
{ label: 'Off', value: SUBTITLE_OFF_VALUE },
...subtitleTracks.value.map((track) => ({ label: track.name, value: track.url })),
]);
const subtitleButtonLabel = computed(() => {
if (!subtitleEnabled.value || !selectedSubtitleTrack.value) {
return 'Subtitles off';
}
return selectedSubtitleTrack.value.name || 'Subtitles';
});
const subtitleSelectValue = computed<string>({
get: () =>
subtitleEnabled.value
? (selectedSubtitleTrackId.value ?? SUBTITLE_OFF_VALUE)
: SUBTITLE_OFF_VALUE,
set: (value: string) => {
if (SUBTITLE_OFF_VALUE === value) {
subtitleEnabled.value = false;
return;
}
selectedSubtitleTrackId.value = value;
subtitleEnabled.value = true;
},
});
useHead(() => (title.value ? { title: formatPageTitle(`Playing: ${title.value}`) } : {}));
watch(
@ -960,6 +1012,10 @@ function updateMediaSessionPosition(target: EventTarget | null) {
}
async function restoreDefaultTextTrack() {
if (true === destroyed.value) {
return;
}
const el = videoElement.value;
if (!el) {
return;
@ -971,36 +1027,6 @@ async function restoreDefaultTextTrack() {
return;
}
const firstTrack = tracksList[0] as TextTrack | undefined;
const needsReload = firstTrack && (!firstTrack.cues || firstTrack.cues.length === 0);
if (needsReload) {
const trackElements = el.querySelectorAll('track');
trackElements.forEach((trackEl, idx) => {
const parent = trackEl.parentNode;
const clone = trackEl.cloneNode(true) as HTMLTrackElement;
trackEl.remove();
setTimeout(() => {
if (parent) {
parent.appendChild(clone);
clone.addEventListener(
'load',
() => {
const trackObj = clone.track;
if (trackObj) {
trackObj.mode = idx === 0 && subtitleEnabled.value ? 'showing' : 'disabled';
}
},
{ once: true },
);
}
}, idx * 10);
});
return;
}
for (let i = 0; i < tracksList.length; i += 1) {
const track = tracksList[i] as TextTrack | undefined;
if (track) {
@ -1010,18 +1036,34 @@ async function restoreDefaultTextTrack() {
await new Promise((resolve) => setTimeout(resolve, 50));
for (let i = 0; i < tracksList.length; i += 1) {
const track = tracksList[i] as TextTrack | undefined;
if (el !== videoElement.value) {
return;
}
if (false === subtitleEnabled.value || null === nativeSubtitleTrack.value) {
return;
}
const activeTracksList = el.textTracks;
for (let i = 0; i < activeTracksList.length; i += 1) {
const track = activeTracksList[i] as TextTrack | undefined;
if (!track) {
continue;
}
track.mode = i === 0 && subtitleEnabled.value ? 'showing' : 'disabled';
track.mode = i === 0 ? 'showing' : 'disabled';
}
} catch (error) {
console.warn('Failed to restore subtitle track state', error);
}
}
function handleNativeTrackLoad() {
if (true === destroyed.value) {
return;
}
void restoreDefaultTextTrack();
}
function applyMediaSessionMetadata() {
if (false === 'mediaSession' in navigator) {
return;
@ -1242,6 +1284,10 @@ watch(subtitleEnabled, () => {
void nextTick(() => restoreDefaultTextTrack());
});
watch(selectedSubtitleTrack, () => {
void nextTick(() => restoreDefaultTextTrack());
});
onMounted(async () => {
disableOpacity();
isTouchDevice.value = window.matchMedia('(pointer: coarse)').matches;

View file

@ -54,7 +54,10 @@ export function usePlayerSubtitles(options: UsePlayerSubtitlesOptions) {
const subtitleLoading = ref(false);
const subtitleLoadError = ref('');
const subtitleEnabled = ref(true);
const selectedTrack = computed(() => tracks.value[0] || null);
const selectedTrackId = ref<string | null>(null);
const selectedTrack = computed(
() => tracks.value.find((track) => track.url === selectedTrackId.value) || null,
);
const nativeSubtitleTrack = computed(() => {
const track = selectedTrack.value;
return subtitleEnabled.value && track?.renderer === 'native' ? track : null;
@ -82,6 +85,7 @@ export function usePlayerSubtitles(options: UsePlayerSubtitlesOptions) {
assRequestId += 1;
destroyAssRenderer();
tracks.value = [];
selectedTrackId.value = null;
subtitleLoadError.value = '';
if (!manifestUrl || !isVideo || !canPlay) {
@ -104,6 +108,7 @@ export function usePlayerSubtitles(options: UsePlayerSubtitlesOptions) {
tracks.value = (payload as SubtitleManifestResponse).subtitles || [];
if (tracks.value.length > 0) {
selectedTrackId.value = tracks.value[0]?.url || null;
subtitleEnabled.value = true;
}
} catch {
@ -212,6 +217,7 @@ export function usePlayerSubtitles(options: UsePlayerSubtitlesOptions) {
subtitleLoadError,
subtitleEnabled,
selectedSubtitleTrack: selectedTrack,
selectedSubtitleTrackId: selectedTrackId,
nativeSubtitleTrack,
usesAssSubtitleTrack: usesAssTrack,
hasSubtitles,