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
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:
parent
e14387e399
commit
60b8e7c4a6
2 changed files with 91 additions and 39 deletions
|
|
@ -94,6 +94,7 @@
|
||||||
:label="nativeSubtitleTrack.name || 'Subtitles'"
|
:label="nativeSubtitleTrack.name || 'Subtitles'"
|
||||||
default
|
default
|
||||||
:src="uri(nativeSubtitleTrack.url)"
|
:src="uri(nativeSubtitleTrack.url)"
|
||||||
|
@load="handleNativeTrackLoad"
|
||||||
/>
|
/>
|
||||||
Your browser does not support the video tag.
|
Your browser does not support the video tag.
|
||||||
</video>
|
</video>
|
||||||
|
|
@ -170,16 +171,30 @@
|
||||||
@click="forceSwitchToHls"
|
@click="forceSwitchToHls"
|
||||||
/>
|
/>
|
||||||
</UTooltip>
|
</UTooltip>
|
||||||
<UButton
|
<USelect
|
||||||
v-if="hasSubtitles"
|
v-if="hasSubtitles"
|
||||||
|
v-model="subtitleSelectValue"
|
||||||
|
:items="subtitleSelectItems"
|
||||||
|
value-key="value"
|
||||||
|
label-key="label"
|
||||||
color="neutral"
|
color="neutral"
|
||||||
variant="soft"
|
variant="soft"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
trailing-icon=""
|
||||||
|
:content="subtitleSelectContent"
|
||||||
|
:portal="helpPortal"
|
||||||
|
:ui="subtitleSelectUi"
|
||||||
class="opacity-65 transition-opacity hover:opacity-100 focus-visible:opacity-100"
|
class="opacity-65 transition-opacity hover:opacity-100 focus-visible:opacity-100"
|
||||||
:icon="subtitleEnabled ? 'i-lucide-captions' : 'i-lucide-captions-off'"
|
:aria-label="subtitleButtonLabel"
|
||||||
:aria-label="subtitleEnabled ? 'Disable subtitles' : 'Enable subtitles'"
|
>
|
||||||
@click="subtitleEnabled = !subtitleEnabled"
|
<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
|
<UButton
|
||||||
color="neutral"
|
color="neutral"
|
||||||
variant="soft"
|
variant="soft"
|
||||||
|
|
@ -429,9 +444,12 @@ const timeLabel = computed(() => {
|
||||||
return `${currentLabel} / ${durationLabel}`;
|
return `${currentLabel} / ${durationLabel}`;
|
||||||
});
|
});
|
||||||
const {
|
const {
|
||||||
|
subtitleTracks,
|
||||||
subtitleLoading,
|
subtitleLoading,
|
||||||
subtitleLoadError,
|
subtitleLoadError,
|
||||||
subtitleEnabled,
|
subtitleEnabled,
|
||||||
|
selectedSubtitleTrack,
|
||||||
|
selectedSubtitleTrackId,
|
||||||
nativeSubtitleTrack,
|
nativeSubtitleTrack,
|
||||||
usesAssSubtitleTrack,
|
usesAssSubtitleTrack,
|
||||||
hasSubtitles,
|
hasSubtitles,
|
||||||
|
|
@ -445,6 +463,40 @@ const {
|
||||||
overlay: assOverlayElement,
|
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}`) } : {}));
|
useHead(() => (title.value ? { title: formatPageTitle(`Playing: ${title.value}`) } : {}));
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
|
@ -960,6 +1012,10 @@ function updateMediaSessionPosition(target: EventTarget | null) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function restoreDefaultTextTrack() {
|
async function restoreDefaultTextTrack() {
|
||||||
|
if (true === destroyed.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const el = videoElement.value;
|
const el = videoElement.value;
|
||||||
if (!el) {
|
if (!el) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -971,36 +1027,6 @@ async function restoreDefaultTextTrack() {
|
||||||
return;
|
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) {
|
for (let i = 0; i < tracksList.length; i += 1) {
|
||||||
const track = tracksList[i] as TextTrack | undefined;
|
const track = tracksList[i] as TextTrack | undefined;
|
||||||
if (track) {
|
if (track) {
|
||||||
|
|
@ -1010,18 +1036,34 @@ async function restoreDefaultTextTrack() {
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||||
|
|
||||||
for (let i = 0; i < tracksList.length; i += 1) {
|
if (el !== videoElement.value) {
|
||||||
const track = tracksList[i] as TextTrack | undefined;
|
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) {
|
if (!track) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
track.mode = i === 0 && subtitleEnabled.value ? 'showing' : 'disabled';
|
track.mode = i === 0 ? 'showing' : 'disabled';
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to restore subtitle track state', error);
|
console.warn('Failed to restore subtitle track state', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleNativeTrackLoad() {
|
||||||
|
if (true === destroyed.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void restoreDefaultTextTrack();
|
||||||
|
}
|
||||||
|
|
||||||
function applyMediaSessionMetadata() {
|
function applyMediaSessionMetadata() {
|
||||||
if (false === 'mediaSession' in navigator) {
|
if (false === 'mediaSession' in navigator) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -1242,6 +1284,10 @@ watch(subtitleEnabled, () => {
|
||||||
void nextTick(() => restoreDefaultTextTrack());
|
void nextTick(() => restoreDefaultTextTrack());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(selectedSubtitleTrack, () => {
|
||||||
|
void nextTick(() => restoreDefaultTextTrack());
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
disableOpacity();
|
disableOpacity();
|
||||||
isTouchDevice.value = window.matchMedia('(pointer: coarse)').matches;
|
isTouchDevice.value = window.matchMedia('(pointer: coarse)').matches;
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,10 @@ export function usePlayerSubtitles(options: UsePlayerSubtitlesOptions) {
|
||||||
const subtitleLoading = ref(false);
|
const subtitleLoading = ref(false);
|
||||||
const subtitleLoadError = ref('');
|
const subtitleLoadError = ref('');
|
||||||
const subtitleEnabled = ref(true);
|
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 nativeSubtitleTrack = computed(() => {
|
||||||
const track = selectedTrack.value;
|
const track = selectedTrack.value;
|
||||||
return subtitleEnabled.value && track?.renderer === 'native' ? track : null;
|
return subtitleEnabled.value && track?.renderer === 'native' ? track : null;
|
||||||
|
|
@ -82,6 +85,7 @@ export function usePlayerSubtitles(options: UsePlayerSubtitlesOptions) {
|
||||||
assRequestId += 1;
|
assRequestId += 1;
|
||||||
destroyAssRenderer();
|
destroyAssRenderer();
|
||||||
tracks.value = [];
|
tracks.value = [];
|
||||||
|
selectedTrackId.value = null;
|
||||||
subtitleLoadError.value = '';
|
subtitleLoadError.value = '';
|
||||||
|
|
||||||
if (!manifestUrl || !isVideo || !canPlay) {
|
if (!manifestUrl || !isVideo || !canPlay) {
|
||||||
|
|
@ -104,6 +108,7 @@ export function usePlayerSubtitles(options: UsePlayerSubtitlesOptions) {
|
||||||
|
|
||||||
tracks.value = (payload as SubtitleManifestResponse).subtitles || [];
|
tracks.value = (payload as SubtitleManifestResponse).subtitles || [];
|
||||||
if (tracks.value.length > 0) {
|
if (tracks.value.length > 0) {
|
||||||
|
selectedTrackId.value = tracks.value[0]?.url || null;
|
||||||
subtitleEnabled.value = true;
|
subtitleEnabled.value = true;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|
@ -212,6 +217,7 @@ export function usePlayerSubtitles(options: UsePlayerSubtitlesOptions) {
|
||||||
subtitleLoadError,
|
subtitleLoadError,
|
||||||
subtitleEnabled,
|
subtitleEnabled,
|
||||||
selectedSubtitleTrack: selectedTrack,
|
selectedSubtitleTrack: selectedTrack,
|
||||||
|
selectedSubtitleTrackId: selectedTrackId,
|
||||||
nativeSubtitleTrack,
|
nativeSubtitleTrack,
|
||||||
usesAssSubtitleTrack: usesAssTrack,
|
usesAssSubtitleTrack: usesAssTrack,
|
||||||
hasSubtitles,
|
hasSubtitles,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue