From 5ca1ae938f40dbff0e31e902643f79b5357b3ece Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Mon, 20 Oct 2025 06:46:27 +0200 Subject: [PATCH] wip: Add subtitles handling --- front/bun.lock | 2 +- .../ui/player/controls/bottom-controls.tsx | 2 +- front/src/ui/player/controls/tracks-menu.tsx | 74 +++++++++++++------ front/src/ui/player/index.tsx | 7 +- transcoder/src/info.go | 4 +- transcoder/src/metadata.go | 9 ++- transcoder/src/subtitles.go | 2 +- transcoder/src/thumbnails.go | 2 +- 8 files changed, 68 insertions(+), 34 deletions(-) diff --git a/front/bun.lock b/front/bun.lock index db4d0a99..34b7d835 100644 --- a/front/bun.lock +++ b/front/bun.lock @@ -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=="], diff --git a/front/src/ui/player/controls/bottom-controls.tsx b/front/src/ui/player/controls/bottom-controls.tsx index f53b21a4..ebf5d141 100644 --- a/front/src/ui/player/controls/bottom-controls.tsx +++ b/front/src/ui/player/controls/bottom-controls.tsx @@ -163,7 +163,7 @@ const ControlButtons = ({ - + diff --git a/front/src/ui/player/controls/tracks-menu.tsx b/front/src/ui/player/controls/tracks-menu.tsx index aa1e4224..4a5c865a 100644 --- a/front/src/ui/player/controls/tracks-menu.tsx +++ b/front/src/ui/player/controls/tracks-menu.tsx @@ -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>>; -export const SubtitleMenu = (props: Partial) => { +export const SubtitleMenu = ({ + player, + ...props +}: { + player: VideoPlayer; +} & Partial) => { const { t } = useTranslation(); + const getDisplayName = useSubtitleName(); + + const rerender = useForceRerender(); + useEvent(player, "onTrackChange", rerender); + + const [slug] = useQueryState("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 ( ) => { {...tooltip(t("player.subtitles"), true)} {...props} > - {/* setSubtitle(null)} */} - {/* /> */} - {/* {subtitles */} - {/* .filter((x) => !!x.link) */} - {/* .map((x, i) => ( */} - {/* setSubtitle(x)} */} - {/* /> */} - {/* ))} */} + select(null, -1)} + /> + {data?.subtitles.map((x, i) => ( + select(x, i)} + /> + ))} ); }; diff --git a/front/src/ui/player/index.tsx b/front/src/ui/player/index.tsx index b1c11b9d..0146ee86 100644 --- a/front/src/ui/player/index.tsx +++ b/front/src/ui/player/index.tsx @@ -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, diff --git a/transcoder/src/info.go b/transcoder/src/info.go index 1d0a882d..5a3f2610 100644 --- a/transcoder/src/info.go +++ b/transcoder/src/info.go @@ -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 diff --git a/transcoder/src/metadata.go b/transcoder/src/metadata.go index 8230bbde..5d33875a 100644 --- a/transcoder/src/metadata.go +++ b/transcoder/src/metadata.go @@ -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) } diff --git a/transcoder/src/subtitles.go b/transcoder/src/subtitles.go index 3a52d58a..69fc4b63 100644 --- a/transcoder/src/subtitles.go +++ b/transcoder/src/subtitles.go @@ -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), ) diff --git a/transcoder/src/thumbnails.go b/transcoder/src/thumbnails.go index 467d98a7..9e8e17eb 100644 --- a/transcoder/src/thumbnails.go +++ b/transcoder/src/thumbnails.go @@ -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)),