mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-02-02 16:38:08 +00:00
feat: Enhance Chromecast functionality and UI improvements
- Implemented a retry mechanism for Chromecast device discovery with a maximum of 3 attempts. - Added logging for discovered devices to aid in debugging. - Updated Chromecast button interactions to streamline navigation to the casting player. - Changed the color scheme for Chromecast components to a consistent purple theme. - Modified the ChromecastDeviceSheet to sync volume slider with prop changes. - Improved the ChromecastSettingsMenu to conditionally render audio and subtitle tracks based on availability. - Updated translations for the casting player to include new strings for better user experience.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -23,27 +23,76 @@ export function Chromecast({
|
||||
background = "transparent",
|
||||
...props
|
||||
}) {
|
||||
const client = useRemoteMediaClient();
|
||||
const castDevice = useCastDevice();
|
||||
const _client = useRemoteMediaClient();
|
||||
const _castDevice = useCastDevice();
|
||||
const devices = useDevices();
|
||||
const sessionManager = GoogleCast.getSessionManager();
|
||||
const _sessionManager = GoogleCast.getSessionManager();
|
||||
const discoveryManager = GoogleCast.getDiscoveryManager();
|
||||
const mediaStatus = useMediaStatus();
|
||||
const api = useAtomValue(apiAtom);
|
||||
const user = useAtomValue(userAtom);
|
||||
|
||||
const lastReportedProgressRef = useRef(0);
|
||||
const discoveryAttempts = useRef(0);
|
||||
const maxDiscoveryAttempts = 3;
|
||||
const hasLoggedDevices = useRef(false);
|
||||
|
||||
// Enhanced discovery with retry mechanism - runs once on mount
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
let isSubscribed = true;
|
||||
let retryTimeout: NodeJS.Timeout;
|
||||
|
||||
const startDiscoveryWithRetry = async () => {
|
||||
if (!discoveryManager) {
|
||||
console.warn("DiscoveryManager is not initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
await discoveryManager.startDiscovery();
|
||||
})();
|
||||
}, [client, devices, castDevice, sessionManager, discoveryManager]);
|
||||
try {
|
||||
// Stop any existing discovery first
|
||||
try {
|
||||
await discoveryManager.stopDiscovery();
|
||||
} catch (_e) {
|
||||
// Ignore errors when stopping
|
||||
}
|
||||
|
||||
// Start fresh discovery
|
||||
await discoveryManager.startDiscovery();
|
||||
discoveryAttempts.current = 0; // Reset on success
|
||||
} catch (error) {
|
||||
console.error("[Chromecast Discovery] Failed:", error);
|
||||
|
||||
// Retry on error
|
||||
if (discoveryAttempts.current < maxDiscoveryAttempts && isSubscribed) {
|
||||
discoveryAttempts.current++;
|
||||
retryTimeout = setTimeout(() => {
|
||||
if (isSubscribed) {
|
||||
startDiscoveryWithRetry();
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
startDiscoveryWithRetry();
|
||||
|
||||
return () => {
|
||||
isSubscribed = false;
|
||||
if (retryTimeout) {
|
||||
clearTimeout(retryTimeout);
|
||||
}
|
||||
};
|
||||
}, [discoveryManager]); // Only re-run if discoveryManager changes
|
||||
|
||||
// Log device changes for debugging - only once per session
|
||||
useEffect(() => {
|
||||
if (devices.length > 0 && !hasLoggedDevices.current) {
|
||||
console.log(
|
||||
"[Chromecast] Found device(s):",
|
||||
devices.map((d) => d.friendlyName || d.deviceId).join(", "),
|
||||
);
|
||||
hasLoggedDevices.current = true;
|
||||
}
|
||||
}, [devices]);
|
||||
|
||||
// Report video progress to Jellyfin server
|
||||
useEffect(() => {
|
||||
@@ -104,13 +153,11 @@ export function Chromecast({
|
||||
<Pressable
|
||||
className='mr-4'
|
||||
onPress={() => {
|
||||
console.log("Chromecast button tapped (iOS)", {
|
||||
hasMediaStatus: !!mediaStatus,
|
||||
currentItemId: mediaStatus?.currentItemId,
|
||||
castDevice: castDevice?.friendlyName,
|
||||
});
|
||||
if (mediaStatus?.currentItemId) router.push("/casting-player");
|
||||
else CastContext.showCastDialog();
|
||||
if (mediaStatus?.currentItemId) {
|
||||
router.push("/casting-player");
|
||||
} else {
|
||||
CastContext.showCastDialog();
|
||||
}
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
@@ -127,14 +174,8 @@ export function Chromecast({
|
||||
className='mr-2'
|
||||
background={false}
|
||||
onPress={() => {
|
||||
console.log("Chromecast button tapped (Android transparent)", {
|
||||
hasMediaStatus: !!mediaStatus,
|
||||
currentItemId: mediaStatus?.currentItemId,
|
||||
castDevice: castDevice?.friendlyName,
|
||||
});
|
||||
if (mediaStatus?.currentItemId) {
|
||||
console.log("Navigating to: /(auth)/casting-player");
|
||||
router.push("/(auth)/casting-player");
|
||||
router.replace("/casting-player" as any);
|
||||
} else {
|
||||
CastContext.showCastDialog();
|
||||
}
|
||||
@@ -150,13 +191,11 @@ export function Chromecast({
|
||||
<RoundButton
|
||||
size='large'
|
||||
onPress={() => {
|
||||
console.log("Chromecast button tapped (Android)", {
|
||||
hasMediaStatus: !!mediaStatus,
|
||||
currentItemId: mediaStatus?.currentItemId,
|
||||
castDevice: castDevice?.friendlyName,
|
||||
});
|
||||
if (mediaStatus?.currentItemId) router.push("/casting-player");
|
||||
else CastContext.showCastDialog();
|
||||
if (mediaStatus?.currentItemId) {
|
||||
router.push("/casting-player");
|
||||
} else {
|
||||
CastContext.showCastDialog();
|
||||
}
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
|
||||
@@ -261,7 +261,7 @@ export const PlayButton: React.FC<Props> = ({
|
||||
if (isOpeningCurrentlyPlayingMedia) {
|
||||
return;
|
||||
}
|
||||
CastContext.showExpandedControls();
|
||||
router.push("/casting-player");
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
|
||||
@@ -47,7 +47,7 @@ export const CastingMiniPlayer: React.FC = () => {
|
||||
);
|
||||
|
||||
const progressPercent = duration > 0 ? (progress / duration) * 100 : 0;
|
||||
const protocolColor = protocol === "chromecast" ? "#F9AB00" : "#666"; // Google yellow
|
||||
const protocolColor = "#a855f7"; // Streamyfin purple
|
||||
|
||||
const handlePress = () => {
|
||||
router.push("/casting-player");
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import React, { useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Modal, Pressable, View } from "react-native";
|
||||
import { Slider } from "react-native-awesome-slider";
|
||||
import type { Device } from "react-native-google-cast";
|
||||
@@ -19,6 +19,7 @@ interface ChromecastDeviceSheetProps {
|
||||
onDisconnect: () => Promise<void>;
|
||||
volume?: number;
|
||||
onVolumeChange?: (volume: number) => Promise<void>;
|
||||
showTechnicalInfo?: boolean;
|
||||
}
|
||||
|
||||
export const ChromecastDeviceSheet: React.FC<ChromecastDeviceSheetProps> = ({
|
||||
@@ -28,11 +29,17 @@ export const ChromecastDeviceSheet: React.FC<ChromecastDeviceSheetProps> = ({
|
||||
onDisconnect,
|
||||
volume = 0.5,
|
||||
onVolumeChange,
|
||||
showTechnicalInfo = false,
|
||||
}) => {
|
||||
const insets = useSafeAreaInsets();
|
||||
const [isDisconnecting, setIsDisconnecting] = useState(false);
|
||||
const volumeValue = useSharedValue(volume * 100);
|
||||
|
||||
// Sync volume slider with prop changes
|
||||
useEffect(() => {
|
||||
volumeValue.value = volume * 100;
|
||||
}, [volume, volumeValue]);
|
||||
|
||||
const handleDisconnect = async () => {
|
||||
setIsDisconnecting(true);
|
||||
try {
|
||||
@@ -55,9 +62,8 @@ export const ChromecastDeviceSheet: React.FC<ChromecastDeviceSheetProps> = ({
|
||||
<Modal
|
||||
visible={visible}
|
||||
animationType='slide'
|
||||
presentationStyle='pageSheet'
|
||||
presentationStyle='formSheet'
|
||||
onRequestClose={onClose}
|
||||
transparent
|
||||
>
|
||||
<Pressable
|
||||
style={{
|
||||
@@ -90,7 +96,7 @@ export const ChromecastDeviceSheet: React.FC<ChromecastDeviceSheetProps> = ({
|
||||
<View
|
||||
style={{ flexDirection: "row", alignItems: "center", gap: 12 }}
|
||||
>
|
||||
<Ionicons name='tv' size={24} color='#e50914' />
|
||||
<Ionicons name='tv' size={24} color='#a855f7' />
|
||||
<Text style={{ color: "white", fontSize: 18, fontWeight: "600" }}>
|
||||
Chromecast
|
||||
</Text>
|
||||
@@ -111,7 +117,7 @@ export const ChromecastDeviceSheet: React.FC<ChromecastDeviceSheetProps> = ({
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{device?.deviceId && (
|
||||
{device?.deviceId && showTechnicalInfo && (
|
||||
<View style={{ marginBottom: 20 }}>
|
||||
<Text style={{ color: "#999", fontSize: 12, marginBottom: 4 }}>
|
||||
Device ID
|
||||
@@ -153,8 +159,8 @@ export const ChromecastDeviceSheet: React.FC<ChromecastDeviceSheetProps> = ({
|
||||
theme={{
|
||||
disableMinTrackTintColor: "#333",
|
||||
maximumTrackTintColor: "#333",
|
||||
minimumTrackTintColor: "#e50914",
|
||||
bubbleBackgroundColor: "#e50914",
|
||||
minimumTrackTintColor: "#a855f7",
|
||||
bubbleBackgroundColor: "#a855f7",
|
||||
}}
|
||||
onValueChange={(value) => {
|
||||
volumeValue.value = value;
|
||||
@@ -171,7 +177,7 @@ export const ChromecastDeviceSheet: React.FC<ChromecastDeviceSheetProps> = ({
|
||||
onPress={handleDisconnect}
|
||||
disabled={isDisconnecting}
|
||||
style={{
|
||||
backgroundColor: "#e50914",
|
||||
backgroundColor: "#a855f7",
|
||||
padding: 16,
|
||||
borderRadius: 8,
|
||||
flexDirection: "row",
|
||||
|
||||
@@ -41,7 +41,7 @@ export const ChromecastEpisodeList: React.FC<ChromecastEpisodeListProps> = ({
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
padding: 12,
|
||||
backgroundColor: isCurrentEpisode ? "#e50914" : "transparent",
|
||||
backgroundColor: isCurrentEpisode ? "#a855f7" : "transparent",
|
||||
borderRadius: 8,
|
||||
marginBottom: 8,
|
||||
}}
|
||||
@@ -131,7 +131,7 @@ export const ChromecastEpisodeList: React.FC<ChromecastEpisodeListProps> = ({
|
||||
<Modal
|
||||
visible={visible}
|
||||
animationType='slide'
|
||||
presentationStyle='pageSheet'
|
||||
presentationStyle='formSheet'
|
||||
onRequestClose={onClose}
|
||||
>
|
||||
<View
|
||||
|
||||
@@ -95,9 +95,8 @@ export const ChromecastSettingsMenu: React.FC<ChromecastSettingsMenuProps> = ({
|
||||
<Modal
|
||||
visible={visible}
|
||||
animationType='slide'
|
||||
presentationStyle='pageSheet'
|
||||
presentationStyle='formSheet'
|
||||
onRequestClose={onClose}
|
||||
transparent
|
||||
>
|
||||
<Pressable
|
||||
style={{
|
||||
@@ -172,16 +171,17 @@ export const ChromecastSettingsMenu: React.FC<ChromecastSettingsMenuProps> = ({
|
||||
)}
|
||||
</View>
|
||||
{selectedMediaSource?.id === source.id && (
|
||||
<Ionicons name='checkmark' size={20} color='#e50914' />
|
||||
<Ionicons name='checkmark' size={20} color='#a855f7' />
|
||||
)}
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Audio Tracks */}
|
||||
{renderSectionHeader("Audio", "musical-notes", "audio")}
|
||||
{expandedSection === "audio" && (
|
||||
{/* Audio Tracks - only show if more than one track */}
|
||||
{audioTracks.length > 1 &&
|
||||
renderSectionHeader("Audio", "musical-notes", "audio")}
|
||||
{audioTracks.length > 1 && expandedSection === "audio" && (
|
||||
<View style={{ paddingVertical: 8 }}>
|
||||
{audioTracks.map((track) => (
|
||||
<Pressable
|
||||
@@ -214,16 +214,17 @@ export const ChromecastSettingsMenu: React.FC<ChromecastSettingsMenuProps> = ({
|
||||
)}
|
||||
</View>
|
||||
{selectedAudioTrack?.index === track.index && (
|
||||
<Ionicons name='checkmark' size={20} color='#e50914' />
|
||||
<Ionicons name='checkmark' size={20} color='#a855f7' />
|
||||
)}
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Subtitle Tracks */}
|
||||
{renderSectionHeader("Subtitles", "text", "subtitles")}
|
||||
{expandedSection === "subtitles" && (
|
||||
{/* Subtitle Tracks - only show if subtitles available */}
|
||||
{subtitleTracks.length > 0 &&
|
||||
renderSectionHeader("Subtitles", "text", "subtitles")}
|
||||
{subtitleTracks.length > 0 && expandedSection === "subtitles" && (
|
||||
<View style={{ paddingVertical: 8 }}>
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
@@ -243,7 +244,7 @@ export const ChromecastSettingsMenu: React.FC<ChromecastSettingsMenuProps> = ({
|
||||
>
|
||||
<Text style={{ color: "white", fontSize: 15 }}>None</Text>
|
||||
{selectedSubtitleTrack === null && (
|
||||
<Ionicons name='checkmark' size={20} color='#e50914' />
|
||||
<Ionicons name='checkmark' size={20} color='#a855f7' />
|
||||
)}
|
||||
</Pressable>
|
||||
{subtitleTracks.map((track) => (
|
||||
@@ -278,7 +279,7 @@ export const ChromecastSettingsMenu: React.FC<ChromecastSettingsMenuProps> = ({
|
||||
)}
|
||||
</View>
|
||||
{selectedSubtitleTrack?.index === track.index && (
|
||||
<Ionicons name='checkmark' size={20} color='#e50914' />
|
||||
<Ionicons name='checkmark' size={20} color='#a855f7' />
|
||||
)}
|
||||
</Pressable>
|
||||
))}
|
||||
@@ -309,7 +310,7 @@ export const ChromecastSettingsMenu: React.FC<ChromecastSettingsMenuProps> = ({
|
||||
{speed === 1 ? "Normal" : `${speed}x`}
|
||||
</Text>
|
||||
{playbackSpeed === speed && (
|
||||
<Ionicons name='checkmark' size={20} color='#e50914' />
|
||||
<Ionicons name='checkmark' size={20} color='#a855f7' />
|
||||
)}
|
||||
</Pressable>
|
||||
))}
|
||||
@@ -343,7 +344,7 @@ export const ChromecastSettingsMenu: React.FC<ChromecastSettingsMenuProps> = ({
|
||||
width: 50,
|
||||
height: 30,
|
||||
borderRadius: 15,
|
||||
backgroundColor: showTechnicalInfo ? "#e50914" : "#333",
|
||||
backgroundColor: showTechnicalInfo ? "#a855f7" : "#333",
|
||||
justifyContent: "center",
|
||||
alignItems: showTechnicalInfo ? "flex-end" : "flex-start",
|
||||
padding: 2,
|
||||
|
||||
@@ -47,7 +47,7 @@ export const ItemPeopleSections: React.FC<Props> = ({ item, ...props }) => {
|
||||
|
||||
return (
|
||||
<MoreMoviesWithActor
|
||||
key={person.Id}
|
||||
key={`${person.Id}-${idx}`}
|
||||
currentItem={item}
|
||||
actorId={person.Id}
|
||||
actorName={person.Name}
|
||||
|
||||
@@ -49,6 +49,21 @@
|
||||
"downloaded_file_no": "No",
|
||||
"downloaded_file_cancel": "Cancel"
|
||||
},
|
||||
"casting_player": {
|
||||
"buffering": "Buffering...",
|
||||
"episodes": "Episodes",
|
||||
"favorite": "Favorite",
|
||||
"next": "Next",
|
||||
"previous": "Previous",
|
||||
"end_playback": "End Playback",
|
||||
"season_episode_format": "Season {{season}} • Episode {{episode}}",
|
||||
"connection_quality": {
|
||||
"excellent": "Excellent",
|
||||
"good": "Good",
|
||||
"fair": "Fair",
|
||||
"poor": "Poor"
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
"enter_url_to_jellyfin_server": "Enter the URL to your Jellyfin server",
|
||||
"server_url_placeholder": "http(s)://your-server.com",
|
||||
|
||||
Reference in New Issue
Block a user