wip: Add subtitles handling

This commit is contained in:
Zoe Roux 2025-10-20 06:46:27 +02:00
parent dfdeca35f3
commit 5ca1ae938f
No known key found for this signature in database
8 changed files with 68 additions and 34 deletions

View File

@ -1265,7 +1265,7 @@
"react-native-svg-transformer": ["react-native-svg-transformer@1.5.1", "", { "dependencies": { "@svgr/core": "^8.1.0", "@svgr/plugin-jsx": "^8.1.0", "@svgr/plugin-svgo": "^8.1.0", "path-dirname": "^1.0.2" }, "peerDependencies": { "react-native": ">=0.59.0", "react-native-svg": ">=12.0.0" } }, "sha512-dFvBNR8A9VPum9KCfh+LE49YiJEF8zUSnEFciKQroR/bEOhlPoZA0SuQ0qNk7m2iZl2w59FYjdRe0pMHWMDl0Q=="],
"react-native-video": ["react-native-video@github:zoriya/react-native-video#8287a84", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-nitro-modules": ">=0.27.2" } }, "zoriya-react-native-video-8287a84"],
"react-native-video": ["react-native-video@github:zoriya/react-native-video#ad69842", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-nitro-modules": ">=0.27.2" } }, "zoriya-react-native-video-ad69842"],
"react-native-web": ["react-native-web@0.21.2", "", { "dependencies": { "@babel/runtime": "^7.18.6", "@react-native/normalize-colors": "^0.74.1", "fbjs": "^3.0.4", "inline-style-prefixer": "^7.0.1", "memoize-one": "^6.0.0", "nullthrows": "^1.1.1", "postcss-value-parser": "^4.2.0", "styleq": "^0.1.3" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg=="],

View File

@ -163,7 +163,7 @@ const ControlButtons = ({
<ProgressText player={player} {...spacing} />
</View>
<View {...css({ flexDirection: "row" })}>
<SubtitleMenu {...menuProps} />
<SubtitleMenu player={player} {...menuProps} />
<AudioMenu player={player} {...menuProps} />
<VideoMenu player={player} {...menuProps} />
<QualityMenu player={player} {...menuProps} />

View File

@ -3,19 +3,50 @@ import MusicNote from "@material-symbols/svg-400/rounded/music_note-fill.svg";
import SettingsIcon from "@material-symbols/svg-400/rounded/settings-fill.svg";
import VideoSettings from "@material-symbols/svg-400/rounded/video_settings-fill.svg";
import { type ComponentProps, createContext, useContext } from "react";
import { useEvent, type VideoPlayer } from "react-native-video";
import { useTranslation } from "react-i18next";
import { IconButton, Menu, tooltip } from "~/primitives";
import { useDisplayName } from "~/track-utils";
import { useEvent, type VideoPlayer } from "react-native-video";
import { useForceRerender } from "yoshiki";
import type { Subtitle } from "~/models";
import { IconButton, Menu, tooltip } from "~/primitives";
import { useFetch } from "~/query";
import { useDisplayName, useSubtitleName } from "~/track-utils";
import { useQueryState } from "~/utils";
import { Player } from "..";
type MenuProps = ComponentProps<typeof Menu<ComponentProps<typeof IconButton>>>;
export const SubtitleMenu = (props: Partial<MenuProps>) => {
export const SubtitleMenu = ({
player,
...props
}: {
player: VideoPlayer;
} & Partial<MenuProps>) => {
const { t } = useTranslation();
const getDisplayName = useSubtitleName();
const rerender = useForceRerender();
useEvent(player, "onTrackChange", rerender);
const [slug] = useQueryState<string>("slug", undefined!);
const { data } = useFetch(Player.infoQuery(slug));
if (data?.subtitles.length === 0) return null;
const selectedIdx = player
.getAvailableTextTracks()
.findIndex((x) => x.selected);
const select = (track: Subtitle | null, idx: number) => {
if (!track) {
player.selectTextTrack(null);
return;
}
// TODO: filter by codec here
const sub = player.getAvailableTextTracks()[idx];
player.selectTextTrack(sub);
};
// {subtitles && subtitles.length > 0 && (
// )}
return (
<Menu
Trigger={IconButton}
@ -23,24 +54,19 @@ export const SubtitleMenu = (props: Partial<MenuProps>) => {
{...tooltip(t("player.subtitles"), true)}
{...props}
>
{/* <Menu.Item */}
{/* label={t("player.subtitle-none")} */}
{/* selected={!selectedSubtitle} */}
{/* onSelect={() => setSubtitle(null)} */}
{/* /> */}
{/* {subtitles */}
{/* .filter((x) => !!x.link) */}
{/* .map((x, i) => ( */}
{/* <Menu.Item */}
{/* key={x.index ?? i} */}
{/* label={ */}
{/* x.link ? getSubtitleName(x) : `${getSubtitleName(x)} (${x.codec})` */}
{/* } */}
{/* selected={selectedSubtitle === x} */}
{/* disabled={!x.link} */}
{/* onSelect={() => setSubtitle(x)} */}
{/* /> */}
{/* ))} */}
<Menu.Item
label={t("player.subtitle-none")}
selected={selectedIdx === -1}
onSelect={() => select(null, -1)}
/>
{data?.subtitles.map((x, i) => (
<Menu.Item
key={x.index ?? x.link}
label={getDisplayName(x)}
selected={i === selectedIdx}
onSelect={() => select(x, i)}
/>
))}
</Menu>
);
};

View File

@ -62,10 +62,13 @@ export const Player = () => {
imageUri: data?.show?.thumbnail?.high ?? undefined,
},
externalSubtitles: info?.subtitles
.filter((x) => x.link)
.filter(
(x) => Platform.OS === "web" || playMode === "hls" || x.isExternal,
)
.map((x) => ({
// we also add those without link to prevent the order from getting out of sync with `info.subtitles`.
// since we never actually play those this is fine
uri: x.link!,
// TODO: translate this `Unknown`
label: x.title ?? "Unknown",
language: x.language ?? "und",
type: x.codec,

View File

@ -287,7 +287,7 @@ func RetriveMediaInfo(path string, sha string) (*MediaInfo, error) {
extension := OrNull(SubtitleExtensions[stream.CodecName])
var link string
if extension != nil {
link = fmt.Sprintf("video/%s/subtitle/%d.%s", base64.RawURLEncoding.EncodeToString([]byte(path)), i, *extension)
link = fmt.Sprintf("/video/%s/subtitle/%d.%s", base64.RawURLEncoding.EncodeToString([]byte(path)), i, *extension)
}
lang, _ := language.Parse(stream.Tags.Language)
idx := uint32(i)
@ -315,7 +315,7 @@ func RetriveMediaInfo(path string, sha string) (*MediaInfo, error) {
}),
Fonts: MapStream(mi.Streams, ffprobe.StreamAttachment, func(stream *ffprobe.Stream, i uint32) string {
font, _ := stream.TagList.GetString("filename")
return fmt.Sprintf("video/%s/attachment/%s", base64.RawURLEncoding.EncodeToString([]byte(path)), font)
return fmt.Sprintf("/video/%s/attachment/%s", base64.RawURLEncoding.EncodeToString([]byte(path)), font)
}),
}
var codecs []string

View File

@ -174,7 +174,7 @@ func (s *MetadataService) GetMetadata(ctx context.Context, path string, sha stri
tx.Exec(`update info set ver_keyframes = 0 where sha = $1`, sha)
err = tx.Commit()
if err != nil {
fmt.Printf("error deleteing old keyframes from database: %v", err)
fmt.Printf("error deleting old keyframes from database: %v", err)
}
}
@ -256,7 +256,7 @@ func (s *MetadataService) getMetadata(path string, sha string) (*MediaInfo, erro
}
if s.Extension != nil {
link := fmt.Sprintf(
"video/%s/subtitle/%d.%s",
"/video/%s/subtitle/%d.%s",
base64.RawURLEncoding.EncodeToString([]byte(ret.Path)),
*s.Index,
*s.Extension,
@ -391,5 +391,10 @@ func (s *MetadataService) storeFreshMetadata(path string, sha string) (*MediaInf
return set(ret, err)
}
err = ret.SearchExternalSubtitles()
if err != nil {
return set(ret, err)
}
return set(ret, nil)
}

View File

@ -31,7 +31,7 @@ outer:
for codec, ext := range SubtitleExtensions {
if strings.HasSuffix(match, ext) {
link := fmt.Sprintf(
"video/%s/direct/%s",
"/video/%s/direct/%s",
base64.RawURLEncoding.EncodeToString([]byte(match)),
filepath.Base(match),
)

View File

@ -145,7 +145,7 @@ func (s *MetadataService) extractThumbnail(ctx context.Context, path string, sha
timestamps := ts
ts += interval
vtt += fmt.Sprintf(
"%s --> %s\nvideo/%s/thumbnails.png#xywh=%d,%d,%d,%d\n\n",
"%s --> %s\n/video/%s/thumbnails.png#xywh=%d,%d,%d,%d\n\n",
tsToVttTime(timestamps),
tsToVttTime(ts),
base64.RawURLEncoding.EncodeToString([]byte(path)),