Skip to content

Commit d85c0ab

Browse files
Merge pull request OwnTube-tv#316 from mykhailodanilenko/feature/captions-support
2 parents 2b8a239 + 619cbcd commit d85c0ab

21 files changed

Lines changed: 338 additions & 29 deletions

File tree

OwnTube.tv/api/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export enum QUERY_KEYS {
2828
playlistVideos = "playlistVideos",
2929
playlistInfo = "playlistInfo",
3030
playlistsCollection = "playlistsCollection",
31+
videoCaptions = "videoCaptions",
3132
}
3233

3334
export const WRONG_SERVER_VERSION_STATUS_CODE = 444;

OwnTube.tv/api/peertubeVideosApi.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { VideosCommonQuery, Video } from "@peertube/peertube-types";
1+
import { VideosCommonQuery, Video, VideoCaption } from "@peertube/peertube-types";
22
import { GetVideosVideo } from "./models";
33
import { commonQueryParams } from "./constants";
44
import { AxiosInstanceBasedApi } from "./axiosInstance";
@@ -174,6 +174,25 @@ export class PeertubeVideosApi extends AxiosInstanceBasedApi {
174174
handleAxiosErrorWithRetry(error, "post video view");
175175
}
176176
}
177+
178+
/**
179+
* Get captions for a specified video
180+
*
181+
* @param [baseURL] - Selected instance url
182+
* @param [id] - Video uuid
183+
* @returns Video captions
184+
*/
185+
async getVideoCaptions(baseURL: string, id: string) {
186+
try {
187+
const response = await this.instance.get<{ data: VideoCaption[] }>(`videos/${id}/captions`, {
188+
baseURL: `https://${baseURL}/api/v1`,
189+
});
190+
191+
return response.data.data;
192+
} catch (error: unknown) {
193+
return handleAxiosErrorWithRetry(error, "video captions");
194+
}
195+
}
177196
}
178197

179198
export const ApiServiceImpl = new PeertubeVideosApi();

OwnTube.tv/api/queries/videos.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,17 @@ export const usePostVideoViewMutation = () => {
122122
},
123123
});
124124
};
125+
126+
export const useGetVideoCaptionsQuery = (id?: string, enabled = true) => {
127+
const { backend } = useLocalSearchParams<RootStackParams["index"]>();
128+
129+
return useQuery({
130+
queryKey: [QUERY_KEYS.videoCaptions, id],
131+
queryFn: async () => {
132+
return await ApiServiceImpl.getVideoCaptions(backend!, id!);
133+
},
134+
refetchOnWindowFocus: false,
135+
enabled: !!backend && !!id && enabled,
136+
retry,
137+
});
138+
};
280 Bytes
Binary file not shown.

OwnTube.tv/assets/selection.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

OwnTube.tv/components/PlaybackSettingsPopup.tsx

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { useMemo, useState } from "react";
55
import { Typography } from "./Typography";
66
import { IcoMoonIcon } from "./IcoMoonIcon";
77
import { Spacer } from "./shared/Spacer";
8-
import { useGetVideoQuery } from "../api";
8+
import { useGetVideoCaptionsQuery, useGetVideoQuery } from "../api";
99
import { useLocalSearchParams } from "expo-router";
1010
import { RootStackParams } from "../app/_layout";
1111
import { ROUTES } from "../types";
@@ -19,6 +19,8 @@ interface PlaybackSettingsPopupProps {
1919
selectedQuality: string;
2020
handleSetQuality: (quality: string) => void;
2121
onSelectOption?: () => void;
22+
handleSetCCLang?: (lang: string) => void;
23+
selectedCCLang?: string;
2224
}
2325

2426
const Setting = ({ name, state, onPress }: { name: string; state?: string; onPress: () => void }) => {
@@ -128,19 +130,25 @@ export const PlaybackSettingsPopup = ({
128130
handleSetSpeed,
129131
handleSetQuality,
130132
onSelectOption,
133+
handleSetCCLang,
134+
selectedCCLang,
131135
}: PlaybackSettingsPopupProps) => {
132136
const { colors } = useTheme();
133137
const { t } = useTranslation();
134138
const [selectedScreen, setSelectedScreen] = useState<keyof typeof screens>("settings");
135139
const { id } = useLocalSearchParams<RootStackParams[ROUTES.VIDEO]>();
136140
const { data: videoData } = useGetVideoQuery({ id, enabled: false });
141+
const { data: videoCaptions } = useGetVideoCaptionsQuery(id, false);
137142

138143
const screens = useMemo(() => {
139144
const qualityOptions = (
140145
videoData?.streamingPlaylists?.[0]?.files?.length ? videoData?.streamingPlaylists?.[0]?.files : videoData?.files
141146
)
142147
?.map(({ resolution }) => ({ ...resolution, id: String(resolution.id) }))
143148
.concat([{ id: "auto", label: t("auto") }]);
149+
const ccOptions = [{ id: "", label: t("off") }].concat(
150+
videoCaptions?.map(({ language }) => ({ id: language.id, label: language.label })) || [],
151+
);
144152

145153
return {
146154
settings: (
@@ -150,10 +158,19 @@ export const PlaybackSettingsPopup = ({
150158
id: "playbackSpeed",
151159
state: selectedSpeed === 1 ? t("normal") : String(selectedSpeed),
152160
},
161+
...(Number(ccOptions?.length) > 1 && Boolean(handleSetCCLang)
162+
? [
163+
{
164+
name: t("subtitlesCC"),
165+
id: "captions",
166+
state: ccOptions?.find(({ id }) => selectedCCLang === id)?.label,
167+
},
168+
]
169+
: []),
153170
{ name: t("quality"), id: "quality", state: qualityOptions?.find(({ id }) => selectedQuality === id)?.label },
154171
] as const
155172
).map(({ name, id, state }) => (
156-
<Setting key={id} name={name} state={state} onPress={() => setSelectedScreen(id)} />
173+
<Setting key={id} name={name} state={state} onPress={() => setSelectedScreen(id as keyof typeof screens)} />
157174
)),
158175
playbackSpeed: (
159176
<>
@@ -189,8 +206,25 @@ export const PlaybackSettingsPopup = ({
189206
))}
190207
</>
191208
),
209+
captions: (
210+
<>
211+
<OptionsHeader text={t("subtitlesCC")} onBackPress={() => setSelectedScreen("settings")} />
212+
{ccOptions?.map(({ id, label }) => (
213+
<Option
214+
onPress={(lang: string) => {
215+
handleSetCCLang?.(lang);
216+
onSelectOption?.();
217+
}}
218+
key={id}
219+
id={id}
220+
isSelected={id === selectedCCLang}
221+
text={label}
222+
/>
223+
))}
224+
</>
225+
),
192226
};
193-
}, [colors, selectedSpeed, selectedQuality, videoData, t]);
227+
}, [colors, selectedSpeed, selectedQuality, videoData, t, selectedCCLang, handleSetCCLang]);
194228

195229
return (
196230
<TVFocusGuideHelper style={[styles.container, { backgroundColor: colors.black80 }]}>

OwnTube.tv/components/VideoControlsOverlay/VideoControlsOverlay.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ export interface VideoControlsOverlayProps {
5858
isChromeCastAvailable?: boolean;
5959
castState?: "airPlay" | "chromecast";
6060
handleLoadGoogleCastMedia?: () => void;
61+
handleToggleCC?: () => void;
62+
isCCAvailable: boolean;
63+
isCCVisible?: boolean;
64+
selectedCCLang?: string;
65+
setSelectedCCLang?: (lang: string) => void;
6166
}
6267

6368
const VideoControlsOverlay = ({
@@ -95,6 +100,11 @@ const VideoControlsOverlay = ({
95100
isChromeCastAvailable,
96101
castState,
97102
handleLoadGoogleCastMedia,
103+
handleToggleCC,
104+
isCCAvailable,
105+
isCCVisible,
106+
selectedCCLang,
107+
setSelectedCCLang,
98108
}: PropsWithChildren<VideoControlsOverlayProps>) => {
99109
const {
100110
isSeekBarFocused,
@@ -206,6 +216,8 @@ const VideoControlsOverlay = ({
206216
selectedQuality={selectedQuality}
207217
handleSetSpeed={handleSetSpeed}
208218
selectedSpeed={speed}
219+
handleSetCCLang={setSelectedCCLang}
220+
selectedCCLang={selectedCCLang}
209221
/>
210222
</View>
211223
)}
@@ -260,6 +272,13 @@ const VideoControlsOverlay = ({
260272
/>
261273
)}
262274
{castState !== "chromecast" && <AvRoutePickerButton isWebAirPlayAvailable={isWebAirPlayAvailable} />}
275+
{isCCAvailable && (
276+
<PlayerButton
277+
color={isCCVisible ? undefined : colors.white25}
278+
icon="Closed-Captions"
279+
onPress={handleToggleCC}
280+
/>
281+
)}
263282
<PlayerButton icon="Settings" onPress={() => setIsSettingsMenuVisible((cur) => !cur)} />
264283
<PlayerButton onPress={toggleFullscreen} icon={`Fullscreen${isFullscreen ? "-Exit" : ""}`} />
265284
</View>

OwnTube.tv/components/VideoControlsOverlay/VideoControlsOverlay.tv.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ const VideoControlsOverlay = ({
6969
speed,
7070
selectedQuality,
7171
handleSetQuality,
72+
handleToggleCC,
73+
isCCAvailable,
7274
}: PropsWithChildren<VideoControlsOverlayProps>) => {
7375
const {
7476
isSeekBarFocused,
@@ -327,11 +329,14 @@ const VideoControlsOverlay = ({
327329
{`${getHumanReadableDuration(position * 1000)} / ${getHumanReadableDuration(duration * 1000)}`}
328330
</Typography>
329331
</View>
330-
<PlayerButton
331-
ref={settingsRef}
332-
icon="Settings"
333-
onPress={() => setIsSettingsMenuVisible((cur) => !cur)}
334-
/>
332+
<TVFocusGuideView style={{ flexDirection: "row" }}>
333+
{isCCAvailable && <PlayerButton icon="Closed-Captions" onPress={handleToggleCC} />}
334+
<PlayerButton
335+
ref={settingsRef}
336+
icon="Settings"
337+
onPress={() => setIsSettingsMenuVisible((cur) => !cur)}
338+
/>
339+
</TVFocusGuideView>
335340
</View>
336341
</LinearGradient>
337342
</View>

OwnTube.tv/components/VideoControlsOverlay/components/PlayerButton/PlayerButton.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ export interface PlayerButtonProps extends PressableProps {
99
onHoverIn?: () => void;
1010
onHoverOut?: () => void;
1111
scale?: number;
12+
color?: string;
1213
}
1314

14-
const PlayerButton = forwardRef<View, PlayerButtonProps>(({ onPress, icon, onHoverIn, onHoverOut }, ref) => {
15+
const PlayerButton = forwardRef<View, PlayerButtonProps>(({ onPress, icon, onHoverIn, onHoverOut, color }, ref) => {
1516
const { colors } = useTheme();
1617
const [isHovered, setIsHovered] = useState(false);
1718

@@ -40,7 +41,7 @@ const PlayerButton = forwardRef<View, PlayerButtonProps>(({ onPress, icon, onHov
4041
},
4142
]}
4243
>
43-
<IcoMoonIcon name={icon} size={spacing.xl} color={isHovered ? colors.white94 : colors.white80} />
44+
<IcoMoonIcon name={icon} size={spacing.xl} color={isHovered ? colors.white94 : color || colors.white80} />
4445
</Pressable>
4546
);
4647
});

0 commit comments

Comments
 (0)