mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-31 20:24:27 -04:00
Move the fonts API into the watch API & Fix chapters API
This commit is contained in:
parent
6eccb2cede
commit
07e8504cea
@ -126,6 +126,11 @@ namespace Kyoo.Abstractions.Models
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public ICollection<Track> Subtitles { get; set; }
|
public ICollection<Track> Subtitles { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The list of fonts that can be used to draw the subtitles.
|
||||||
|
/// </summary>
|
||||||
|
public ICollection<Font> Fonts { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The list of chapters. See <see cref="Chapter"/> for more information.
|
/// The list of chapters. See <see cref="Chapter"/> for more information.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -138,8 +143,10 @@ namespace Kyoo.Abstractions.Models
|
|||||||
/// <param name="library">
|
/// <param name="library">
|
||||||
/// A library manager to retrieve the next and previous episode and load the show and tracks of the episode.
|
/// A library manager to retrieve the next and previous episode and load the show and tracks of the episode.
|
||||||
/// </param>
|
/// </param>
|
||||||
|
/// <param name="fs">A file system used to retrieve chapters informations.</param>
|
||||||
|
/// <param name="transcoder">The transcoder used to list fonts.</param>
|
||||||
/// <returns>A new WatchItem representing the given episode.</returns>
|
/// <returns>A new WatchItem representing the given episode.</returns>
|
||||||
public static async Task<WatchItem> FromEpisode(Episode ep, ILibraryManager library)
|
public static async Task<WatchItem> FromEpisode(Episode ep, ILibraryManager library, IFileSystem fs, ITranscoder transcoder)
|
||||||
{
|
{
|
||||||
await library.Load(ep, x => x.Show);
|
await library.Load(ep, x => x.Show);
|
||||||
await library.Load(ep, x => x.Tracks);
|
await library.Load(ep, x => x.Tracks);
|
||||||
@ -204,37 +211,51 @@ namespace Kyoo.Abstractions.Models
|
|||||||
Video = ep.Tracks.FirstOrDefault(x => x.Type == StreamType.Video),
|
Video = ep.Tracks.FirstOrDefault(x => x.Type == StreamType.Video),
|
||||||
Audios = ep.Tracks.Where(x => x.Type == StreamType.Audio).ToArray(),
|
Audios = ep.Tracks.Where(x => x.Type == StreamType.Audio).ToArray(),
|
||||||
Subtitles = ep.Tracks.Where(x => x.Type == StreamType.Subtitle).ToArray(),
|
Subtitles = ep.Tracks.Where(x => x.Type == StreamType.Subtitle).ToArray(),
|
||||||
|
Fonts = await transcoder.ListFonts(ep),
|
||||||
PreviousEpisode = previous,
|
PreviousEpisode = previous,
|
||||||
NextEpisode = next,
|
NextEpisode = next,
|
||||||
Chapters = await _GetChapters(ep.Path),
|
Chapters = await _GetChapters(ep, fs),
|
||||||
IsMovie = ep.Show.IsMovie
|
IsMovie = ep.Show.IsMovie
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO move this method in a controller to support abstraction.
|
// TODO move this method in a controller to support abstraction.
|
||||||
// TODO use a IFileManager to retrieve and read files.
|
private static async Task<ICollection<Chapter>> _GetChapters(Episode episode, IFileSystem fs)
|
||||||
private static async Task<ICollection<Chapter>> _GetChapters(string episodePath)
|
|
||||||
{
|
{
|
||||||
string path = PathIO.Combine(
|
string path = fs.Combine(
|
||||||
PathIO.GetDirectoryName(episodePath)!,
|
await fs.GetExtraDirectory(episode),
|
||||||
"Chapters",
|
"Chapters",
|
||||||
PathIO.GetFileNameWithoutExtension(episodePath) + ".txt"
|
PathIO.GetFileNameWithoutExtension(episode.Path) + ".txt"
|
||||||
);
|
);
|
||||||
if (!File.Exists(path))
|
if (!await fs.Exists(path))
|
||||||
return Array.Empty<Chapter>();
|
return Array.Empty<Chapter>();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return (await File.ReadAllLinesAsync(path))
|
using StreamReader sr = new(await fs.GetReader(path));
|
||||||
|
string chapters = await sr.ReadToEndAsync();
|
||||||
|
return chapters.Split('\n')
|
||||||
.Select(x =>
|
.Select(x =>
|
||||||
{
|
{
|
||||||
string[] values = x.Split(' ');
|
string[] values = x.Split(' ');
|
||||||
return new Chapter(float.Parse(values[0]), float.Parse(values[1]), string.Join(' ', values.Skip(2)));
|
if (
|
||||||
|
values.Length < 3
|
||||||
|
|| !float.TryParse(values[0], out float start)
|
||||||
|
|| !float.TryParse(values[1], out float end)
|
||||||
|
)
|
||||||
|
return null;
|
||||||
|
return new Chapter(
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
string.Join(' ', values.Skip(2))
|
||||||
|
);
|
||||||
})
|
})
|
||||||
|
.Where(x => x != null)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
}
|
}
|
||||||
catch
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
await Console.Error.WriteLineAsync($"Invalid chapter file at {path}");
|
await Console.Error.WriteLineAsync($"Invalid chapter file at {path}");
|
||||||
|
Console.Error.WriteLine(ex.ToString());
|
||||||
return Array.Empty<Chapter>();
|
return Array.Empty<Chapter>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -172,60 +172,5 @@ namespace Kyoo.Core.Api
|
|||||||
return BadRequest(new RequestError(ex.Message));
|
return BadRequest(new RequestError(ex.Message));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// List fonts
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// List available fonts for this episode.
|
|
||||||
/// </remarks>
|
|
||||||
/// <param name="identifier">The ID or slug of the <see cref="Episode"/>.</param>
|
|
||||||
/// <returns>An object containing the name of the font followed by the url to retrieve it.</returns>
|
|
||||||
[HttpGet("{identifier:id}/fonts")]
|
|
||||||
[HttpGet("{identifier:id}/font", Order = AlternativeRoute)]
|
|
||||||
[PartialPermission(Kind.Read)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
||||||
public async Task<ActionResult<ICollection<Font>>> GetFonts(Identifier identifier)
|
|
||||||
{
|
|
||||||
Episode episode = await identifier.Match(
|
|
||||||
id => _libraryManager.GetOrDefault<Episode>(id),
|
|
||||||
slug => _libraryManager.GetOrDefault<Episode>(slug)
|
|
||||||
);
|
|
||||||
if (episode == null)
|
|
||||||
return NotFound();
|
|
||||||
return Ok(await _transcoder.ListFonts(episode));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get font
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Get a font file that is used in subtitles of this episode.
|
|
||||||
/// </remarks>
|
|
||||||
/// <param name="identifier">The ID or slug of the <see cref="Episode"/>.</param>
|
|
||||||
/// <param name="slug">The slug of the font to retrieve.</param>
|
|
||||||
/// <returns>A page of collections.</returns>
|
|
||||||
/// <response code="404">No show with the given ID/slug could be found or the font does not exist.</response>
|
|
||||||
[HttpGet("{identifier:id}/fonts/{slug}")]
|
|
||||||
[HttpGet("{identifier:id}/font/{slug}", Order = AlternativeRoute)]
|
|
||||||
[PartialPermission(Kind.Read)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
||||||
public async Task<IActionResult> GetFont(Identifier identifier, string slug)
|
|
||||||
{
|
|
||||||
Episode episode = await identifier.Match(
|
|
||||||
id => _libraryManager.GetOrDefault<Episode>(id),
|
|
||||||
slug => _libraryManager.GetOrDefault<Episode>(slug)
|
|
||||||
);
|
|
||||||
if (episode == null)
|
|
||||||
return NotFound();
|
|
||||||
if (slug.Contains('.'))
|
|
||||||
slug = slug[..slug.LastIndexOf('.')];
|
|
||||||
Font font = await _transcoder.GetFont(episode, slug);
|
|
||||||
if (font == null)
|
|
||||||
return NotFound();
|
|
||||||
return _files.FileResult(font.Path);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,15 +45,29 @@ namespace Kyoo.Core.Api
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly ILibraryManager _libraryManager;
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A file system used to retrieve chapters informations.
|
||||||
|
/// </summary>
|
||||||
|
private readonly IFileSystem _files;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The transcoder used to list fonts.
|
||||||
|
/// </summary>
|
||||||
|
private readonly ITranscoder _transcoder;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new <see cref="WatchApi"/>.
|
/// Create a new <see cref="WatchApi"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="libraryManager">
|
/// <param name="libraryManager">
|
||||||
/// The library manager used to modify or retrieve information in the data store.
|
/// The library manager used to modify or retrieve information in the data store.
|
||||||
/// </param>
|
/// </param>
|
||||||
public WatchApi(ILibraryManager libraryManager)
|
/// <param name="fs">A file system used to retrieve chapters informations.</param>
|
||||||
|
/// <param name="transcoder">The transcoder used to list fonts.</param>
|
||||||
|
public WatchApi(ILibraryManager libraryManager, IFileSystem fs, ITranscoder transcoder)
|
||||||
{
|
{
|
||||||
_libraryManager = libraryManager;
|
_libraryManager = libraryManager;
|
||||||
|
_files = fs;
|
||||||
|
_transcoder = transcoder;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -77,7 +91,38 @@ namespace Kyoo.Core.Api
|
|||||||
);
|
);
|
||||||
if (item == null)
|
if (item == null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
return await WatchItem.FromEpisode(item, _libraryManager);
|
return await WatchItem.FromEpisode(item, _libraryManager, _files, _transcoder);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get font
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Get a font file that is used in subtitles of this episode.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="identifier">The ID or slug of the <see cref="Episode"/>.</param>
|
||||||
|
/// <param name="slug">The slug of the font to retrieve.</param>
|
||||||
|
/// <returns>A page of collections.</returns>
|
||||||
|
/// <response code="404">No show with the given ID/slug could be found or the font does not exist.</response>
|
||||||
|
[HttpGet("{identifier:id}/fonts/{slug}")]
|
||||||
|
[HttpGet("{identifier:id}/font/{slug}", Order = AlternativeRoute)]
|
||||||
|
[PartialPermission(Kind.Read)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<IActionResult> GetFont(Identifier identifier, string slug)
|
||||||
|
{
|
||||||
|
Episode episode = await identifier.Match(
|
||||||
|
id => _libraryManager.GetOrDefault<Episode>(id),
|
||||||
|
slug => _libraryManager.GetOrDefault<Episode>(slug)
|
||||||
|
);
|
||||||
|
if (episode == null)
|
||||||
|
return NotFound();
|
||||||
|
if (slug.Contains('.'))
|
||||||
|
slug = slug[..slug.LastIndexOf('.')];
|
||||||
|
Font font = await _transcoder.GetFont(episode, slug);
|
||||||
|
if (font == null)
|
||||||
|
return NotFound();
|
||||||
|
return _files.FileResult(font.Path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,6 +62,22 @@ export const TrackP = ResourceP.extend({
|
|||||||
});
|
});
|
||||||
export type Track = z.infer<typeof TrackP>;
|
export type Track = z.infer<typeof TrackP>;
|
||||||
|
|
||||||
|
export const FontP = z.object({
|
||||||
|
/*
|
||||||
|
* A human-readable identifier, used in the URL.
|
||||||
|
*/
|
||||||
|
slug: z.string(),
|
||||||
|
/*
|
||||||
|
* The name of the font file (with the extension).
|
||||||
|
*/
|
||||||
|
file: z.string(),
|
||||||
|
/*
|
||||||
|
* The format of this font (the extension).
|
||||||
|
*/
|
||||||
|
format: z.string(),
|
||||||
|
});
|
||||||
|
export type Font = z.infer<typeof FontP>;
|
||||||
|
|
||||||
export const ChapterP = z.object({
|
export const ChapterP = z.object({
|
||||||
/**
|
/**
|
||||||
* The start time of the chapter (in second from the start of the episode).
|
* The start time of the chapter (in second from the start of the episode).
|
||||||
@ -119,6 +135,10 @@ const WatchMovieP = z.preprocess(
|
|||||||
* The list of subtitles tracks. See Track for more information.
|
* The list of subtitles tracks. See Track for more information.
|
||||||
*/
|
*/
|
||||||
subtitles: z.array(TrackP),
|
subtitles: z.array(TrackP),
|
||||||
|
/**
|
||||||
|
* The list of fonts that can be used to display subtitles.
|
||||||
|
*/
|
||||||
|
fonts: z.array(FontP),
|
||||||
/**
|
/**
|
||||||
* The list of chapters. See Chapter for more information.
|
* The list of chapters. See Chapter for more information.
|
||||||
*/
|
*/
|
||||||
|
@ -45,6 +45,7 @@ import { Person, PersonP } from "~/models";
|
|||||||
import { PersonAvatar } from "~/components/person";
|
import { PersonAvatar } from "~/components/person";
|
||||||
import { ErrorComponent, ErrorPage } from "~/components/errors";
|
import { ErrorComponent, ErrorPage } from "~/components/errors";
|
||||||
import { HorizontalList } from "~/components/horizontal-list";
|
import { HorizontalList } from "~/components/horizontal-list";
|
||||||
|
import NextLink from "next/link";
|
||||||
|
|
||||||
const StudioText = ({
|
const StudioText = ({
|
||||||
studio,
|
studio,
|
||||||
@ -149,9 +150,11 @@ export const ShowHeader = ({ data }: { data?: Show | Movie }) => {
|
|||||||
)}
|
)}
|
||||||
<Box sx={{ "& > *": { m: ".3rem !important" } }}>
|
<Box sx={{ "& > *": { m: ".3rem !important" } }}>
|
||||||
<Tooltip title={t("show.play")}>
|
<Tooltip title={t("show.play")}>
|
||||||
<Fab color="primary" size="small" aria-label={t("show.play")}>
|
<NextLink href={data ? `/watch/${data.slug}` : ""} passHref>
|
||||||
<PlayArrow />
|
<Fab color="primary" size="small" aria-label={t("show.play")}>
|
||||||
</Fab>
|
<PlayArrow />
|
||||||
|
</Fab>
|
||||||
|
</NextLink>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title={t("show.trailer")} aria-label={t("show.trailer")}>
|
<Tooltip title={t("show.trailer")} aria-label={t("show.trailer")}>
|
||||||
<IconButton>
|
<IconButton>
|
||||||
|
@ -20,18 +20,10 @@
|
|||||||
|
|
||||||
import { QueryIdentifier, QueryPage } from "~/utils/query";
|
import { QueryIdentifier, QueryPage } from "~/utils/query";
|
||||||
import { withRoute } from "~/utils/router";
|
import { withRoute } from "~/utils/router";
|
||||||
import { WatchItem, WatchItemP, Chapter, Track } from "~/models/resources/watch-item";
|
import { WatchItem, WatchItemP, Chapter, Track, Font } from "~/models/resources/watch-item";
|
||||||
import { useFetch } from "~/utils/query";
|
import { useFetch } from "~/utils/query";
|
||||||
import { ErrorPage } from "~/components/errors";
|
import { ErrorPage } from "~/components/errors";
|
||||||
import {
|
import { useState, useRef, useEffect, memo, useMemo, useCallback, RefObject } from "react";
|
||||||
useState,
|
|
||||||
useRef,
|
|
||||||
useEffect,
|
|
||||||
memo,
|
|
||||||
useMemo,
|
|
||||||
useCallback,
|
|
||||||
RefObject,
|
|
||||||
} from "react";
|
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
@ -43,7 +35,7 @@ import {
|
|||||||
Menu,
|
Menu,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
BoxProps,
|
BoxProps,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import useTranslation from "next-translate/useTranslation";
|
import useTranslation from "next-translate/useTranslation";
|
||||||
import {
|
import {
|
||||||
@ -66,7 +58,7 @@ import { Link } from "~/utils/link";
|
|||||||
import NextLink from "next/link";
|
import NextLink from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import SubtitleOctopus from "@jellyfin/libass-wasm"
|
import SubtitleOctopus from "@jellyfin/libass-wasm";
|
||||||
|
|
||||||
const toTimerString = (timer: number, duration?: number) => {
|
const toTimerString = (timer: number, duration?: number) => {
|
||||||
if (!duration) duration = timer;
|
if (!duration) duration = timer;
|
||||||
@ -264,9 +256,9 @@ const ProgressBar = ({
|
|||||||
key={x.startTime}
|
key={x.startTime}
|
||||||
sx={{
|
sx={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
width: "2px",
|
width: "4px",
|
||||||
top: 0,
|
top: 0,
|
||||||
botton: 0,
|
bottom: 0,
|
||||||
left: `${(x.startTime / duration) * 100}%`,
|
left: `${(x.startTime / duration) * 100}%`,
|
||||||
background: (theme) => theme.palette.primary.dark,
|
background: (theme) => theme.palette.primary.dark,
|
||||||
}}
|
}}
|
||||||
@ -311,12 +303,13 @@ const LeftButtons = memo(function LeftButtons({
|
|||||||
setVolume: (value: number) => void;
|
setVolume: (value: number) => void;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation("player");
|
const { t } = useTranslation("player");
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: "flex", "> *": { mx: "8px !important" } }}>
|
<Box sx={{ display: "flex", "> *": { mx: "8px !important" } }}>
|
||||||
{previousSlug && (
|
{previousSlug && (
|
||||||
<Tooltip title={t("previous")}>
|
<Tooltip title={t("previous")}>
|
||||||
<NextLink href={`/watch/${previousSlug}`} passHref>
|
<NextLink href={{query: { ...router.query, slug: previousSlug }}} passHref>
|
||||||
<IconButton aria-label={t("previous")} sx={{ color: "white" }}>
|
<IconButton aria-label={t("previous")} sx={{ color: "white" }}>
|
||||||
<SkipPrevious />
|
<SkipPrevious />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@ -334,7 +327,7 @@ const LeftButtons = memo(function LeftButtons({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
{nextSlug && (
|
{nextSlug && (
|
||||||
<Tooltip title={t("next")}>
|
<Tooltip title={t("next")}>
|
||||||
<NextLink href={`/watch/${nextSlug}`} passHref>
|
<NextLink href={{query: { ...router.query, slug: nextSlug }}} passHref>
|
||||||
<IconButton aria-label={t("next")} sx={{ color: "white" }}>
|
<IconButton aria-label={t("next")} sx={{ color: "white" }}>
|
||||||
<SkipNext />
|
<SkipNext />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@ -466,65 +459,78 @@ const Back = memo(function Back({ name, href }: { name?: string; href: string })
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const useSubtitleController = (player: RefObject<HTMLVideoElement>): [Track | null, (value: Track | null) => void] => {
|
const useSubtitleController = (
|
||||||
|
player: RefObject<HTMLVideoElement>,
|
||||||
|
slug: string,
|
||||||
|
fonts?: Font[],
|
||||||
|
subtitles?: Track[],
|
||||||
|
): [Track | null, (value: Track | null) => void] => {
|
||||||
const [selectedSubtitle, setSubtitle] = useState<Track | null>(null);
|
const [selectedSubtitle, setSubtitle] = useState<Track | null>(null);
|
||||||
const [htmlTrack, setHtmlTrack] = useState<HTMLTrackElement | null>(null);
|
const [htmlTrack, setHtmlTrack] = useState<HTMLTrackElement | null>(null);
|
||||||
const [subocto, setSubOcto] = useState<SubtitleOctopus | null>(null);
|
const [subocto, setSubOcto] = useState<SubtitleOctopus | null>(null);
|
||||||
|
const { query: { subtitle } } = useRouter();
|
||||||
|
|
||||||
return [
|
const selectSubtitle = useCallback(
|
||||||
selectedSubtitle,
|
(value: Track | null) => {
|
||||||
useCallback(
|
const removeHtmlSubtitle = () => {
|
||||||
(value: Track | null) => {
|
if (htmlTrack) htmlTrack.remove();
|
||||||
const removeHtmlSubtitle = () => {
|
setHtmlTrack(null);
|
||||||
if (htmlTrack) htmlTrack.remove();
|
};
|
||||||
setHtmlTrack(null);
|
const removeOctoSub = () => {
|
||||||
};
|
if (subocto) {
|
||||||
const removeOctoSub = () => {
|
subocto.freeTrack();
|
||||||
if (subocto) {
|
subocto.dispose();
|
||||||
subocto.freeTrack();
|
|
||||||
subocto.dispose();
|
|
||||||
}
|
|
||||||
setSubOcto(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!player.current) return;
|
|
||||||
|
|
||||||
setSubtitle(value);
|
|
||||||
if (!value) {
|
|
||||||
removeHtmlSubtitle();
|
|
||||||
removeOctoSub();
|
|
||||||
} else if (value.codec === "vtt" || value.codec === "srt") {
|
|
||||||
removeOctoSub();
|
|
||||||
const track: HTMLTrackElement = htmlTrack ?? document.createElement("track");
|
|
||||||
track.kind = "subtitles";
|
|
||||||
track.label = value.displayName;
|
|
||||||
if (value.language) track.srclang = value.language;
|
|
||||||
track.src = `subtitle/${value.slug}.vtt`;
|
|
||||||
track.className = "subtitle_container";
|
|
||||||
track.default = true;
|
|
||||||
track.onload = () => {
|
|
||||||
if (player.current) player.current.textTracks[0].mode = "showing";
|
|
||||||
};
|
|
||||||
player.current.appendChild(track);
|
|
||||||
setHtmlTrack(track);
|
|
||||||
} else if (value.codec === "ass") {
|
|
||||||
removeHtmlSubtitle();
|
|
||||||
removeOctoSub();
|
|
||||||
setSubOcto(
|
|
||||||
new SubtitleOctopus({
|
|
||||||
video: player.current,
|
|
||||||
subUrl: `/api/subtitle/${value.slug}`,
|
|
||||||
workerUrl: "/_next/static/chunks/subtitles-octopus-worker.js",
|
|
||||||
legacyWorkerUrl: "/_next/static/chunks/subtitles-octopus-worker-legacy.js",
|
|
||||||
/* fonts: */
|
|
||||||
renderMode: "wasm-blend",
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
setSubOcto(null);
|
||||||
[htmlTrack, subocto, player],
|
};
|
||||||
),
|
|
||||||
];
|
if (!player.current) return;
|
||||||
|
|
||||||
|
setSubtitle(value);
|
||||||
|
if (!value) {
|
||||||
|
removeHtmlSubtitle();
|
||||||
|
removeOctoSub();
|
||||||
|
} else if (value.codec === "vtt" || value.codec === "srt") {
|
||||||
|
removeOctoSub();
|
||||||
|
const track: HTMLTrackElement = htmlTrack ?? document.createElement("track");
|
||||||
|
track.kind = "subtitles";
|
||||||
|
track.label = value.displayName;
|
||||||
|
if (value.language) track.srclang = value.language;
|
||||||
|
track.src = `subtitle/${value.slug}.vtt`;
|
||||||
|
track.className = "subtitle_container";
|
||||||
|
track.default = true;
|
||||||
|
track.onload = () => {
|
||||||
|
if (player.current) player.current.textTracks[0].mode = "showing";
|
||||||
|
};
|
||||||
|
player.current.appendChild(track);
|
||||||
|
setHtmlTrack(track);
|
||||||
|
} else if (value.codec === "ass") {
|
||||||
|
removeHtmlSubtitle();
|
||||||
|
removeOctoSub();
|
||||||
|
setSubOcto(
|
||||||
|
new SubtitleOctopus({
|
||||||
|
video: player.current,
|
||||||
|
subUrl: `/api/subtitle/${value.slug}`,
|
||||||
|
workerUrl: "/_next/static/chunks/subtitles-octopus-worker.js",
|
||||||
|
legacyWorkerUrl: "/_next/static/chunks/subtitles-octopus-worker-legacy.js",
|
||||||
|
fonts: fonts?.map((x) => `/api/watch/${slug}/font/${x.slug}.${x.format}`),
|
||||||
|
renderMode: "wasm-blend",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[htmlTrack, subocto, player, fonts, slug],
|
||||||
|
);
|
||||||
|
|
||||||
|
const newSub = subtitles?.find(x => x.language === subtitle);
|
||||||
|
useEffect(() => {
|
||||||
|
if (newSub === undefined) return;
|
||||||
|
console.log("old", selectedSubtitle)
|
||||||
|
console.log("new", newSub)
|
||||||
|
if (newSub?.id !== selectedSubtitle?.id) selectSubtitle(newSub);
|
||||||
|
}, [player.current?.src, newSub, selectedSubtitle, selectSubtitle]);
|
||||||
|
|
||||||
|
return [selectedSubtitle, selectSubtitle];
|
||||||
};
|
};
|
||||||
|
|
||||||
const useVideoController = () => {
|
const useVideoController = () => {
|
||||||
@ -537,7 +543,12 @@ const useVideoController = () => {
|
|||||||
const [volume, setVolume] = useState(100);
|
const [volume, setVolume] = useState(100);
|
||||||
const [isMuted, setMute] = useState(false);
|
const [isMuted, setMute] = useState(false);
|
||||||
const [isFullscreen, setFullscreen] = useState(false);
|
const [isFullscreen, setFullscreen] = useState(false);
|
||||||
const [selectedSubtitle, selectSubtitle] = useSubtitleController(player);
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!player.current) return;
|
||||||
|
if (player.current.paused) player.current.play();
|
||||||
|
setPlay(!player.current.paused);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!player?.current?.duration) return;
|
if (!player?.current?.duration) return;
|
||||||
@ -590,6 +601,7 @@ const useVideoController = () => {
|
|||||||
[player, togglePlay, toggleFullscreen],
|
[player, togglePlay, toggleFullscreen],
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
|
playerRef: player,
|
||||||
state: {
|
state: {
|
||||||
isPlaying,
|
isPlaying,
|
||||||
isLoading,
|
isLoading,
|
||||||
@ -599,7 +611,6 @@ const useVideoController = () => {
|
|||||||
volume,
|
volume,
|
||||||
isMuted,
|
isMuted,
|
||||||
isFullscreen,
|
isFullscreen,
|
||||||
selectedSubtitle,
|
|
||||||
},
|
},
|
||||||
videoProps,
|
videoProps,
|
||||||
togglePlay,
|
togglePlay,
|
||||||
@ -621,7 +632,6 @@ const useVideoController = () => {
|
|||||||
},
|
},
|
||||||
[player],
|
[player],
|
||||||
),
|
),
|
||||||
selectSubtitle,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -637,25 +647,21 @@ let mouseCallback: NodeJS.Timeout;
|
|||||||
const Player: QueryPage<{ slug: string }> = ({ slug }) => {
|
const Player: QueryPage<{ slug: string }> = ({ slug }) => {
|
||||||
const { data, error } = useFetch(query(slug));
|
const { data, error } = useFetch(query(slug));
|
||||||
const {
|
const {
|
||||||
state: {
|
playerRef,
|
||||||
isPlaying,
|
state: { isPlaying, isLoading, progress, duration, buffered, volume, isMuted, isFullscreen },
|
||||||
isLoading,
|
|
||||||
progress,
|
|
||||||
duration,
|
|
||||||
buffered,
|
|
||||||
volume,
|
|
||||||
isMuted,
|
|
||||||
isFullscreen,
|
|
||||||
selectedSubtitle,
|
|
||||||
},
|
|
||||||
videoProps,
|
videoProps,
|
||||||
togglePlay,
|
togglePlay,
|
||||||
toggleMute,
|
toggleMute,
|
||||||
toggleFullscreen,
|
toggleFullscreen,
|
||||||
setProgress,
|
setProgress,
|
||||||
setVolume,
|
setVolume,
|
||||||
selectSubtitle,
|
|
||||||
} = useVideoController();
|
} = useVideoController();
|
||||||
|
const [selectedSubtitle, selectSubtitle] = useSubtitleController(
|
||||||
|
playerRef,
|
||||||
|
slug,
|
||||||
|
data?.fonts,
|
||||||
|
data?.subtitles,
|
||||||
|
);
|
||||||
const [showHover, setHover] = useState(false);
|
const [showHover, setHover] = useState(false);
|
||||||
const [mouseMoved, setMouseMoved] = useState(false);
|
const [mouseMoved, setMouseMoved] = useState(false);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user