fix: remove chromecast

This commit is contained in:
Fredrik Burmester
2025-01-02 09:49:57 +01:00
parent 349a86bcfb
commit 093cd94455
13 changed files with 32 additions and 416 deletions

View File

@@ -1,4 +1,3 @@
import { Chromecast } from "@/components/Chromecast";
import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack";
import { Feather } from "@expo/vector-icons";
import { Stack, useRouter } from "expo-router";
@@ -19,7 +18,6 @@ export default function IndexLayout() {
headerShadowVisible: false,
headerRight: () => (
<View className="flex flex-row items-center space-x-2">
<Chromecast />
<TouchableOpacity
onPress={() => {
router.push("/(auth)/settings");

View File

@@ -1,4 +1,3 @@
import { Chromecast } from "@/components/Chromecast";
import { ItemImage } from "@/components/common/ItemImage";
import { Text } from "@/components/common/Text";
import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
@@ -28,16 +27,6 @@ export default function page() {
const navigation = useNavigation();
useEffect(() => {
navigation.setOptions({
headerRight: () => (
<View className="">
<Chromecast />
</View>
),
});
});
const { data: album } = useQuery({
queryKey: ["album", albumId, artistId],
queryFn: async () => {

View File

@@ -306,7 +306,6 @@ const Player = () => {
isPlaying: isPlaying,
togglePlay: togglePlay,
stopPlayback: stop,
offline: false,
});
const [selectedTextTrack, setSelectedTextTrack] = useState<

View File

@@ -1,83 +0,0 @@
import { Feather } from "@expo/vector-icons";
import { BlurView } from "expo-blur";
import React, { useCallback, useEffect } from "react";
import { Platform, TouchableOpacity, ViewProps } from "react-native";
import GoogleCast, {
CastButton,
CastContext,
useCastDevice,
useDevices,
useMediaStatus,
useRemoteMediaClient,
} from "react-native-google-cast";
import { RoundButton } from "./RoundButton";
interface Props extends ViewProps {
width?: number;
height?: number;
background?: "blur" | "transparent";
}
export const Chromecast: React.FC<Props> = ({
width = 48,
height = 48,
background = "transparent",
...props
}) => {
const client = useRemoteMediaClient();
const castDevice = useCastDevice();
const devices = useDevices();
const sessionManager = GoogleCast.getSessionManager();
const discoveryManager = GoogleCast.getDiscoveryManager();
const mediaStatus = useMediaStatus();
useEffect(() => {
(async () => {
if (!discoveryManager) {
return;
}
await discoveryManager.startDiscovery();
})();
}, [client, devices, castDevice, sessionManager, discoveryManager]);
// Android requires the cast button to be present for startDiscovery to work
const AndroidCastButton = useCallback(
() =>
Platform.OS === "android" ? (
<CastButton tintColor="transparent" />
) : (
<></>
),
[Platform.OS]
);
if (background === "transparent")
return (
<RoundButton
size="large"
className="mr-2"
background={false}
onPress={() => {
if (mediaStatus?.currentItemId) CastContext.showExpandedControls();
else CastContext.showCastDialog();
}}
{...props}
>
<Feather name="cast" size={22} color={"white"} />
</RoundButton>
);
return (
<RoundButton
size="large"
onPress={() => {
if (mediaStatus?.currentItemId) CastContext.showExpandedControls();
else CastContext.showCastDialog();
}}
{...props}
>
<Feather name="cast" size={22} color={"white"} />
</RoundButton>
);
};

View File

@@ -28,7 +28,6 @@ import { useAtom } from "jotai";
import React, { useEffect, useMemo, useState } from "react";
import { View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Chromecast } from "./Chromecast";
import { ItemHeader } from "./ItemHeader";
import { ItemTechnicalDetails } from "./ItemTechnicalDetails";
import { MediaSourceSelector } from "./MediaSourceSelector";
@@ -84,7 +83,6 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
headerRight: () =>
item && (
<View className="flex flex-row items-center space-x-2">
<Chromecast background="blur" width={22} height={22} />
{item.Type !== "Program" && (
<View className="flex flex-row items-center space-x-2">
<PlayedStatus item={item} />

View File

@@ -1,24 +1,15 @@
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { itemThemeColorAtom } from "@/utils/atoms/primaryColor";
import { useSettings } from "@/utils/atoms/settings";
import { getParentBackdropImageUrl } from "@/utils/jellyfin/image/getParentBackdropImageUrl";
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
import ios from "@/utils/profiles/ios";
import { runtimeTicksToMinutes } from "@/utils/time";
import { useActionSheet } from "@expo/react-native-action-sheet";
import { Feather, Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import * as Haptics from "expo-haptics";
import { useRouter } from "expo-router";
import { useAtom, useAtomValue } from "jotai";
import { useCallback, useEffect } from "react";
import { Alert, TouchableOpacity, View } from "react-native";
import CastContext, {
CastButton,
PlayServicesState,
useMediaStatus,
useRemoteMediaClient,
} from "react-native-google-cast";
import { TouchableOpacity, View } from "react-native";
import Animated, {
Easing,
interpolate,
@@ -31,8 +22,6 @@ import Animated, {
} from "react-native-reanimated";
import { Button } from "./Button";
import { SelectedOptions } from "./ItemContent";
import { chromecastProfile } from "@/utils/profiles/chromecast";
import * as Haptics from "expo-haptics";
interface Props extends React.ComponentProps<typeof Button> {
item: BaseItemDto;
@@ -48,8 +37,6 @@ export const PlayButton: React.FC<Props> = ({
...props
}: Props) => {
const { showActionSheetWithOptions } = useActionSheet();
const client = useRemoteMediaClient();
const mediaStatus = useMediaStatus();
const [colorAtom] = useAtom(itemThemeColorAtom);
const api = useAtomValue(apiAtom);
@@ -91,137 +78,16 @@ export const PlayButton: React.FC<Props> = ({
const queryString = queryParams.toString();
if (!client) {
goToPlayer(queryString, selectedOptions.bitrate?.value);
return;
}
goToPlayer(queryString, selectedOptions.bitrate?.value);
const options = ["Chromecast", "Device", "Cancel"];
const cancelButtonIndex = 2;
showActionSheetWithOptions(
{
options,
cancelButtonIndex,
},
async (selectedIndex: number | undefined) => {
if (!api) return;
const currentTitle = mediaStatus?.mediaInfo?.metadata?.title;
const isOpeningCurrentlyPlayingMedia =
currentTitle && currentTitle === item?.Name;
switch (selectedIndex) {
case 0:
await CastContext.getPlayServicesState().then(async (state) => {
if (state && state !== PlayServicesState.SUCCESS)
CastContext.showPlayServicesErrorDialog(state);
else {
// Get a new URL with the Chromecast device profile:
const data = await getStreamUrl({
api,
item,
deviceProfile: chromecastProfile,
startTimeTicks: item?.UserData?.PlaybackPositionTicks!,
userId: user?.Id,
audioStreamIndex: selectedOptions.audioIndex,
maxStreamingBitrate: selectedOptions.bitrate?.value,
mediaSourceId: selectedOptions.mediaSource?.Id,
subtitleStreamIndex: selectedOptions.subtitleIndex,
});
if (!data?.url) {
console.warn("No URL returned from getStreamUrl", data);
Alert.alert(
"Client error",
"Could not create stream for Chromecast"
);
return;
}
client
.loadMedia({
mediaInfo: {
contentUrl: data?.url,
contentType: "video/mp4",
metadata:
item.Type === "Episode"
? {
type: "tvShow",
title: item.Name || "",
episodeNumber: item.IndexNumber || 0,
seasonNumber: item.ParentIndexNumber || 0,
seriesTitle: item.SeriesName || "",
images: [
{
url: getParentBackdropImageUrl({
api,
item,
quality: 90,
width: 2000,
})!,
},
],
}
: item.Type === "Movie"
? {
type: "movie",
title: item.Name || "",
subtitle: item.Overview || "",
images: [
{
url: getPrimaryImageUrl({
api,
item,
quality: 90,
width: 2000,
})!,
},
],
}
: {
type: "generic",
title: item.Name || "",
subtitle: item.Overview || "",
images: [
{
url: getPrimaryImageUrl({
api,
item,
quality: 90,
width: 2000,
})!,
},
],
},
},
startTime: 0,
})
.then(() => {
// state is already set when reopening current media, so skip it here.
if (isOpeningCurrentlyPlayingMedia) {
return;
}
CastContext.showExpandedControls();
});
}
});
break;
case 1:
goToPlayer(queryString, selectedOptions.bitrate?.value);
break;
case cancelButtonIndex:
break;
}
}
);
return;
}, [
item,
client,
settings,
api,
user,
router,
showActionSheetWithOptions,
mediaStatus,
selectedOptions,
]);
@@ -355,21 +221,13 @@ export const PlayButton: React.FC<Props> = ({
<Animated.Text style={animatedTextStyle}>
<Ionicons name="play-circle" size={24} />
</Animated.Text>
{client && (
<Animated.Text style={animatedTextStyle}>
<Feather name="cast" size={22} />
<CastButton tintColor="transparent" />
</Animated.Text>
)}
{!client && settings?.openInVLC && (
<Animated.Text style={animatedTextStyle}>
<MaterialCommunityIcons
name="vlc"
size={18}
color={animatedTextStyle.color}
/>
</Animated.Text>
)}
<Animated.Text style={animatedTextStyle}>
<MaterialCommunityIcons
name="vlc"
size={18}
color={animatedTextStyle.color}
/>
</Animated.Text>
</View>
</View>
</TouchableOpacity>

View File

@@ -9,7 +9,7 @@ import {
import * as Haptics from "expo-haptics";
interface Props extends TouchableOpacityProps {
onPress?: () => void,
onPress?: () => void;
icon?: keyof typeof Ionicons.glyphMap;
background?: boolean;
size?: "default" | "large";
@@ -98,7 +98,6 @@ export const RoundButton: React.FC<PropsWithChildren<Props>> = ({
<BlurView
intensity={90}
className={`rounded-full overflow-hidden ${buttonSize} flex items-center justify-center ${fillColorClass}`}
{...props}
>
{icon ? (
<Ionicons

View File

@@ -1,7 +1,7 @@
import { Text } from "@/components/common/Text";
import { FontAwesome, Ionicons } from "@expo/vector-icons";
import { useQuery } from "@tanstack/react-query";
import { useEffect, useState } from "react";
import { useState } from "react";
import { TouchableOpacity, View, ViewProps } from "react-native";
import { FilterSheet } from "./FilterSheet";

View File

@@ -8,11 +8,6 @@ import { useRouter } from "expo-router";
import { useAtom } from "jotai";
import { useCallback } from "react";
import { TouchableOpacity, TouchableOpacityProps, View } from "react-native";
import CastContext, {
PlayServicesState,
useCastDevice,
useRemoteMediaClient,
} from "react-native-google-cast";
interface Props extends TouchableOpacityProps {
collectionId: string;
@@ -32,40 +27,14 @@ export const SongsListItem: React.FC<Props> = ({
}) => {
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const castDevice = useCastDevice();
const router = useRouter();
const client = useRemoteMediaClient();
const { showActionSheetWithOptions } = useActionSheet();
const { setPlaySettings } = usePlaySettings();
const openSelect = () => {
if (!castDevice?.deviceId) {
play("device");
return;
}
const options = ["Chromecast", "Device", "Cancel"];
const cancelButtonIndex = 2;
showActionSheetWithOptions(
{
options,
cancelButtonIndex,
},
(selectedIndex: number | undefined) => {
switch (selectedIndex) {
case 0:
play("cast");
break;
case 1:
play("device");
break;
case cancelButtonIndex:
break;
}
}
);
play("device");
return;
};
const play = useCallback(async (type: "device" | "cast") => {

View File

@@ -1,25 +1,25 @@
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { runtimeTicksToSeconds } from "@/utils/time";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { atom, useAtom } from "jotai";
import { useEffect, useMemo, useState, useRef } from "react";
import { View, TouchableOpacity } from "react-native";
import { getTvShowsApi } from "@jellyfin/sdk/lib/utils/api";
import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
import { Ionicons } from "@expo/vector-icons";
import { Loader } from "@/components/Loader";
import ContinueWatchingPoster from "@/components/ContinueWatchingPoster";
import { Text } from "@/components/common/Text";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Loader } from "@/components/Loader";
import {
HorizontalScroll,
HorizontalScrollRef,
} from "@/components/common/HorrizontalScroll";
import { Text } from "@/components/common/Text";
import {
SeasonDropdown,
SeasonIndexState,
} from "@/components/series/SeasonDropdown";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
import { runtimeTicksToSeconds } from "@/utils/time";
import { Ionicons } from "@expo/vector-icons";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { getTvShowsApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { atom, useAtom } from "jotai";
import { useEffect, useMemo, useRef, useState } from "react";
import { TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
type Props = {
item: BaseItemDto;
@@ -32,7 +32,6 @@ export const seasonIndexAtom = atom<SeasonIndexState>({});
export const EpisodeList: React.FC<Props> = ({ item, close, goToItem }) => {
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const insets = useSafeAreaInsets(); // Get safe area insets
const [seasonIndexState, setSeasonIndexState] = useAtom(seasonIndexAtom);
const scrollViewRef = useRef<HorizontalScrollRef>(null); // Reference to the HorizontalScroll
const scrollToIndex = (index: number) => {

View File

@@ -1,14 +0,0 @@
import React, { createContext } from "react";
import { useJobProcessor } from "@/utils/atoms/queue";
const JobQueueContext = createContext(null);
export const JobQueueProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
useJobProcessor();
return (
<JobQueueContext.Provider value={null}>{children}</JobQueueContext.Provider>
);
};

View File

@@ -3,15 +3,9 @@
"compilerOptions": {
"strict": true,
"paths": {
"@/*": [
"./*"
]
"@/*": ["./*"]
}
},
"include": [
"**/*.ts",
"**/*.tsx",
".expo/types/**/*.ts",
"expo-env.d.ts"
]
"include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"],
"exclude": ["node_modules", "utils/jellyseerr/**/*.ts"]
}

View File

@@ -1,90 +0,0 @@
import { DeviceProfile } from "@jellyfin/sdk/lib/generated-client/models";
export const chromecastProfile: DeviceProfile = {
Name: "Chromecast Video Profile",
MaxStreamingBitrate: 8000000, // 8 Mbps
MaxStaticBitrate: 8000000, // 8 Mbps
MusicStreamingTranscodingBitrate: 384000, // 384 kbps
CodecProfiles: [
{
Type: "Video",
Codec: "h264",
},
{
Type: "Audio",
Codec: "aac,mp3,flac,opus,vorbis",
},
],
DirectPlayProfiles: [
{
Container: "mp4",
Type: "Video",
VideoCodec: "h264",
AudioCodec: "aac,mp3,opus,vorbis",
},
{
Container: "mp3",
Type: "Audio",
},
{
Container: "aac",
Type: "Audio",
},
{
Container: "flac",
Type: "Audio",
},
{
Container: "wav",
Type: "Audio",
},
],
TranscodingProfiles: [
{
Container: "ts",
Type: "Video",
VideoCodec: "h264",
AudioCodec: "aac,mp3",
Protocol: "hls",
Context: "Streaming",
MaxAudioChannels: "2",
MinSegments: 2,
BreakOnNonKeyFrames: true,
},
{
Container: "mp4",
Type: "Video",
VideoCodec: "h264",
AudioCodec: "aac",
Protocol: "http",
Context: "Streaming",
MaxAudioChannels: "2",
},
{
Container: "mp3",
Type: "Audio",
AudioCodec: "mp3",
Protocol: "http",
Context: "Streaming",
MaxAudioChannels: "2",
},
{
Container: "aac",
Type: "Audio",
AudioCodec: "aac",
Protocol: "http",
Context: "Streaming",
MaxAudioChannels: "2",
},
],
SubtitleProfiles: [
{
Format: "vtt",
Method: "Encode",
},
{
Format: "vtt",
Method: "Encode",
},
],
};