mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-02-10 06:12:23 +00:00
fix: Refactors casting player and components
Refactors the casting player screen and related components for improved code clarity, performance, and maintainability. - Removes unused code and simplifies logic, especially around audio track selection and recommended stereo track handling. - Improves the formatting of trickplay time displays for consistency. - Streamlines UI elements and removes unnecessary conditional checks. - Updates the Chromecast component to use hooks for side effects, ensuring the Chromecast session remains active. - Improves the display of the language in the audio track display.
This commit is contained in:
@@ -47,11 +47,11 @@ import { useSettings } from "@/utils/atoms/settings";
|
||||
import {
|
||||
calculateEndingTime,
|
||||
formatTime,
|
||||
formatTrickplayTime,
|
||||
getPosterUrl,
|
||||
truncateTitle,
|
||||
} from "@/utils/casting/helpers";
|
||||
import { buildCastMediaInfo } from "@/utils/casting/mediaInfo";
|
||||
import type { CastProtocol } from "@/utils/casting/types";
|
||||
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
|
||||
import { chromecast } from "@/utils/profiles/chromecast";
|
||||
import { chromecasth265 } from "@/utils/profiles/chromecasth265";
|
||||
@@ -180,7 +180,6 @@ export default function CastingPlayerScreen() {
|
||||
}, [fetchedItem, mediaStatus?.mediaInfo]);
|
||||
|
||||
// Derive state from raw Chromecast hooks
|
||||
const protocol: CastProtocol = "chromecast";
|
||||
const progress = liveProgress; // Use live-updating progress
|
||||
const duration = mediaStatus?.mediaInfo?.streamDuration ?? 0;
|
||||
const isPlaying = mediaStatus?.playerState === MediaPlayerState.PLAYING;
|
||||
@@ -241,7 +240,7 @@ export default function CastingPlayerScreen() {
|
||||
const [selectedSubtitleTrackIndex, setSelectedSubtitleTrackIndex] = useState<
|
||||
number | null
|
||||
>(null);
|
||||
const [currentPlaybackSpeed, setCurrentPlaybackSpeed] = useState(1.0);
|
||||
const [currentPlaybackSpeed, setCurrentPlaybackSpeed] = useState(1);
|
||||
|
||||
// Function to reload media with new audio/subtitle/quality settings
|
||||
const reloadWithSettings = useCallback(
|
||||
@@ -399,7 +398,11 @@ export default function CastingPlayerScreen() {
|
||||
language: stream.Language || "Unknown",
|
||||
displayTitle:
|
||||
stream.DisplayTitle ||
|
||||
`${stream.Language || "Unknown"}${stream.IsForced ? " (Forced)" : ""}${stream.Title ? ` - ${stream.Title}` : ""}`,
|
||||
[
|
||||
stream.Language || "Unknown",
|
||||
stream.IsForced ? " (Forced)" : "",
|
||||
stream.Title ? ` - ${stream.Title}` : "",
|
||||
].join(""),
|
||||
codec: stream.Codec || "Unknown",
|
||||
isForced: stream.IsForced || false,
|
||||
isExternal: stream.IsExternal || false,
|
||||
@@ -450,43 +453,6 @@ export default function CastingPlayerScreen() {
|
||||
return variants;
|
||||
}, [currentItem?.MediaSources, currentItem?.MediaStreams, currentItem?.Id]);
|
||||
|
||||
// Track whether user has manually selected an audio track
|
||||
const [userSelectedAudio, setUserSelectedAudio] = useState(false);
|
||||
|
||||
// Detect recommended stereo track for Chromecast compatibility.
|
||||
// Does NOT mutate selectedAudioTrackIndex — UI can show a badge instead.
|
||||
// TODO: Use recommendedAudioTrackIndex in UI to show a "stereo recommended" badge
|
||||
const [_recommendedAudioTrackIndex, setRecommendedAudioTrackIndex] = useState<
|
||||
number | null
|
||||
>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!remoteMediaClient || !mediaStatus?.mediaInfo || userSelectedAudio) {
|
||||
setRecommendedAudioTrackIndex(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTrack = availableAudioTracks.find(
|
||||
(t) => t.index === selectedAudioTrackIndex,
|
||||
);
|
||||
|
||||
// If current track is 5.1+ audio, recommend stereo alternative
|
||||
if (currentTrack && (currentTrack.channels || 0) > 2) {
|
||||
const stereoTrack = availableAudioTracks.find((t) => t.channels === 2);
|
||||
if (stereoTrack && stereoTrack.index !== selectedAudioTrackIndex) {
|
||||
setRecommendedAudioTrackIndex(stereoTrack.index);
|
||||
return;
|
||||
}
|
||||
}
|
||||
setRecommendedAudioTrackIndex(null);
|
||||
}, [
|
||||
mediaStatus?.mediaInfo,
|
||||
availableAudioTracks,
|
||||
remoteMediaClient,
|
||||
selectedAudioTrackIndex,
|
||||
userSelectedAudio,
|
||||
]);
|
||||
|
||||
// Fetch episodes for TV shows
|
||||
useEffect(() => {
|
||||
if (currentItem?.Type !== "Episode" || !currentItem.SeriesId || !api)
|
||||
@@ -532,7 +498,7 @@ export default function CastingPlayerScreen() {
|
||||
useEffect(() => {
|
||||
if (mediaStatus?.currentItemId && !currentItem) {
|
||||
// New media started casting while we're not on the player
|
||||
router.replace("/casting-player" as "/casting-player");
|
||||
router.replace("/casting-player" as const);
|
||||
}
|
||||
}, [mediaStatus?.currentItemId, currentItem, router]);
|
||||
|
||||
@@ -907,11 +873,9 @@ export default function CastingPlayerScreen() {
|
||||
fontWeight: "600",
|
||||
}}
|
||||
>
|
||||
{currentSegment.type === "intro"
|
||||
? t("player.skip_intro")
|
||||
: currentSegment.type === "credits"
|
||||
? t("player.skip_outro")
|
||||
: `Skip ${currentSegment.type}`}
|
||||
{t(
|
||||
`player.skip_${currentSegment.type === "credits" ? "outro" : currentSegment.type}`,
|
||||
)}
|
||||
</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
@@ -1174,11 +1138,7 @@ export default function CastingPlayerScreen() {
|
||||
fontWeight: "600",
|
||||
}}
|
||||
>
|
||||
{`${trickplayTime.hours > 0 ? `${trickplayTime.hours}:` : ""}${
|
||||
trickplayTime.minutes < 10
|
||||
? `0${trickplayTime.minutes}`
|
||||
: trickplayTime.minutes
|
||||
}:${trickplayTime.seconds < 10 ? `0${trickplayTime.seconds}` : trickplayTime.seconds}`}
|
||||
{formatTrickplayTime(trickplayTime)}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
@@ -1257,11 +1217,7 @@ export default function CastingPlayerScreen() {
|
||||
fontWeight: "600",
|
||||
}}
|
||||
>
|
||||
{`${trickplayTime.hours > 0 ? `${trickplayTime.hours}:` : ""}${
|
||||
trickplayTime.minutes < 10
|
||||
? `0${trickplayTime.minutes}`
|
||||
: trickplayTime.minutes
|
||||
}:${trickplayTime.seconds < 10 ? `0${trickplayTime.seconds}` : trickplayTime.seconds}`}
|
||||
{formatTrickplayTime(trickplayTime)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
@@ -1319,7 +1275,7 @@ export default function CastingPlayerScreen() {
|
||||
color='white'
|
||||
style={{ transform: [{ scaleY: -1 }, { rotate: "180deg" }] }}
|
||||
/>
|
||||
{settings?.rewindSkipTime && (
|
||||
{!!settings?.rewindSkipTime && (
|
||||
<Text
|
||||
style={{
|
||||
position: "absolute",
|
||||
@@ -1364,7 +1320,7 @@ export default function CastingPlayerScreen() {
|
||||
}}
|
||||
>
|
||||
<Ionicons name='refresh-outline' size={48} color='white' />
|
||||
{settings?.forwardSkipTime && (
|
||||
{!!settings?.forwardSkipTime && (
|
||||
<Text
|
||||
style={{
|
||||
position: "absolute",
|
||||
@@ -1383,10 +1339,10 @@ export default function CastingPlayerScreen() {
|
||||
|
||||
{/* Modals */}
|
||||
<ChromecastDeviceSheet
|
||||
visible={showDeviceSheet && protocol === "chromecast"}
|
||||
visible={showDeviceSheet}
|
||||
onClose={() => setShowDeviceSheet(false)}
|
||||
device={
|
||||
currentDevice && protocol === "chromecast" && castDevice
|
||||
currentDevice && castDevice
|
||||
? { friendlyName: currentDevice }
|
||||
: null
|
||||
}
|
||||
@@ -1414,7 +1370,7 @@ export default function CastingPlayerScreen() {
|
||||
volume={volume}
|
||||
onVolumeChange={async (vol) => {
|
||||
try {
|
||||
await setVolume(vol);
|
||||
setVolume(vol);
|
||||
} catch (error) {
|
||||
console.error("[Casting Player] Failed to set volume:", error);
|
||||
}
|
||||
@@ -1448,25 +1404,24 @@ export default function CastingPlayerScreen() {
|
||||
}}
|
||||
audioTracks={availableAudioTracks}
|
||||
selectedAudioTrack={
|
||||
selectedAudioTrackIndex !== null
|
||||
? availableAudioTracks.find(
|
||||
selectedAudioTrackIndex === null
|
||||
? availableAudioTracks[0] || null
|
||||
: availableAudioTracks.find(
|
||||
(t) => t.index === selectedAudioTrackIndex,
|
||||
) || null
|
||||
: availableAudioTracks[0] || null
|
||||
}
|
||||
onAudioTrackChange={(track) => {
|
||||
setUserSelectedAudio(true);
|
||||
setSelectedAudioTrackIndex(track.index);
|
||||
// Reload stream with new audio track
|
||||
reloadWithSettings({ audioIndex: track.index });
|
||||
}}
|
||||
subtitleTracks={availableSubtitleTracks}
|
||||
selectedSubtitleTrack={
|
||||
selectedSubtitleTrackIndex !== null
|
||||
? availableSubtitleTracks.find(
|
||||
selectedSubtitleTrackIndex === null
|
||||
? null
|
||||
: availableSubtitleTracks.find(
|
||||
(t) => t.index === selectedSubtitleTrackIndex,
|
||||
) || null
|
||||
: null
|
||||
}
|
||||
onSubtitleTrackChange={(track) => {
|
||||
setSelectedSubtitleTrackIndex(track?.index ?? null);
|
||||
|
||||
@@ -26,11 +26,11 @@ export function Chromecast({
|
||||
background = "transparent",
|
||||
...props
|
||||
}) {
|
||||
const _client = useRemoteMediaClient();
|
||||
const _castDevice = useCastDevice();
|
||||
// Hooks called for their side effects (keep Chromecast session active)
|
||||
useRemoteMediaClient();
|
||||
useCastDevice();
|
||||
const castState = useCastState();
|
||||
const devices = useDevices();
|
||||
const _sessionManager = GoogleCast.getSessionManager();
|
||||
useDevices();
|
||||
const discoveryManager = GoogleCast.getDiscoveryManager();
|
||||
const mediaStatus = useMediaStatus();
|
||||
const api = useAtomValue(apiAtom);
|
||||
@@ -46,7 +46,6 @@ export function Chromecast({
|
||||
const lastContentIdRef = useRef<string | null>(null);
|
||||
const discoveryAttempts = useRef(0);
|
||||
const maxDiscoveryAttempts = 3;
|
||||
const hasLoggedDevices = useRef(false);
|
||||
|
||||
// Enhanced discovery with retry mechanism - runs once on mount
|
||||
useEffect(() => {
|
||||
@@ -62,7 +61,7 @@ export function Chromecast({
|
||||
// Stop any existing discovery first
|
||||
try {
|
||||
await discoveryManager.stopDiscovery();
|
||||
} catch (_e) {
|
||||
} catch {
|
||||
// Ignore errors when stopping
|
||||
}
|
||||
|
||||
@@ -94,25 +93,9 @@ export function Chromecast({
|
||||
};
|
||||
}, [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(() => {
|
||||
if (
|
||||
!api ||
|
||||
!user?.Id ||
|
||||
!mediaStatus ||
|
||||
!mediaStatus.mediaInfo?.contentId
|
||||
) {
|
||||
if (!api || !user?.Id || !mediaStatus?.mediaInfo?.contentId) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,11 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { useTrickplay } from "@/hooks/useTrickplay";
|
||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||
import { formatTime, getPosterUrl } from "@/utils/casting/helpers";
|
||||
import {
|
||||
formatTime,
|
||||
formatTrickplayTime,
|
||||
getPosterUrl,
|
||||
} from "@/utils/casting/helpers";
|
||||
import { CASTING_CONSTANTS } from "@/utils/casting/types";
|
||||
import { msToTicks, ticksToSeconds } from "@/utils/time";
|
||||
|
||||
@@ -129,7 +133,8 @@ export const CastingMiniPlayer: React.FC = () => {
|
||||
) {
|
||||
// Build season poster URL using SeriesId and image tag for cache validation
|
||||
const imageTag = currentItem.ImageTags?.Primary || "";
|
||||
return `${api.basePath}/Items/${currentItem.SeriesId}/Images/Primary?fillHeight=120&fillWidth=80&quality=96${imageTag ? `&tag=${imageTag}` : ""}`;
|
||||
const tagParam = imageTag ? `&tag=${imageTag}` : "";
|
||||
return `${api.basePath}/Items/${currentItem.SeriesId}/Images/Primary?fillHeight=120&fillWidth=80&quality=96${tagParam}`;
|
||||
}
|
||||
|
||||
// For non-episodes, use item's own poster
|
||||
@@ -273,11 +278,7 @@ export const CastingMiniPlayer: React.FC = () => {
|
||||
<Text
|
||||
style={{ color: "#fff", fontSize: 11, fontWeight: "600" }}
|
||||
>
|
||||
{`${trickplayTime.hours > 0 ? `${trickplayTime.hours}:` : ""}${
|
||||
trickplayTime.minutes < 10
|
||||
? `0${trickplayTime.minutes}`
|
||||
: trickplayTime.minutes
|
||||
}:${trickplayTime.seconds < 10 ? `0${trickplayTime.seconds}` : trickplayTime.seconds}`}
|
||||
{formatTrickplayTime(trickplayTime)}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
@@ -347,11 +348,7 @@ export const CastingMiniPlayer: React.FC = () => {
|
||||
<Text
|
||||
style={{ color: "#fff", fontSize: 10, fontWeight: "600" }}
|
||||
>
|
||||
{`${trickplayTime.hours > 0 ? `${trickplayTime.hours}:` : ""}${
|
||||
trickplayTime.minutes < 10
|
||||
? `0${trickplayTime.minutes}`
|
||||
: trickplayTime.minutes
|
||||
}:${trickplayTime.seconds < 10 ? `0${trickplayTime.seconds}` : trickplayTime.seconds}`}
|
||||
{formatTrickplayTime(trickplayTime)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -26,7 +26,6 @@ import { DEFAULT_CAST_STATE } from "@/utils/casting/types";
|
||||
export const useCasting = (item: BaseItemDto | null) => {
|
||||
const api = useAtomValue(apiAtom);
|
||||
const user = useAtomValue(userAtom);
|
||||
// const { settings } = useSettings(); // TODO: Use for preferences
|
||||
|
||||
// Chromecast hooks
|
||||
const client = useRemoteMediaClient();
|
||||
@@ -36,7 +35,6 @@ export const useCasting = (item: BaseItemDto | null) => {
|
||||
|
||||
// Local state
|
||||
const [state, setState] = useState<CastPlayerState>(DEFAULT_CAST_STATE);
|
||||
const controlsTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const lastReportedProgressRef = useRef(0);
|
||||
const volumeDebounceRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const hasReportedStartRef = useRef<string | null>(null); // Track which item we reported start for
|
||||
@@ -347,34 +345,9 @@ export const useCasting = (item: BaseItemDto | null) => {
|
||||
[client, activeProtocol, isConnected],
|
||||
);
|
||||
|
||||
// Controls visibility
|
||||
const showControls = useCallback(() => {
|
||||
updateState((prev) => ({ ...prev, showControls: true }));
|
||||
|
||||
if (controlsTimeoutRef.current) {
|
||||
clearTimeout(controlsTimeoutRef.current);
|
||||
}
|
||||
controlsTimeoutRef.current = setTimeout(() => {
|
||||
// Read latest isPlaying from stateRef to avoid stale closure
|
||||
if (stateRef.current.isPlaying) {
|
||||
updateState((prev) => ({ ...prev, showControls: false }));
|
||||
}
|
||||
}, 5000);
|
||||
}, [updateState]);
|
||||
|
||||
const hideControls = useCallback(() => {
|
||||
updateState((prev) => ({ ...prev, showControls: false }));
|
||||
if (controlsTimeoutRef.current) {
|
||||
clearTimeout(controlsTimeoutRef.current);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Cleanup
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (controlsTimeoutRef.current) {
|
||||
clearTimeout(controlsTimeoutRef.current);
|
||||
}
|
||||
if (volumeDebounceRef.current) {
|
||||
clearTimeout(volumeDebounceRef.current);
|
||||
}
|
||||
@@ -395,7 +368,6 @@ export const useCasting = (item: BaseItemDto | null) => {
|
||||
|
||||
// Availability
|
||||
isChromecastAvailable: true, // Always available via react-native-google-cast
|
||||
// Future: Add availability checks for other protocols
|
||||
|
||||
// Raw clients (for advanced operations)
|
||||
remoteMediaClient: client,
|
||||
@@ -409,7 +381,5 @@ export const useCasting = (item: BaseItemDto | null) => {
|
||||
skipBackward,
|
||||
stop,
|
||||
setVolume,
|
||||
showControls,
|
||||
hideControls,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
* Common utilities for casting protocols
|
||||
*/
|
||||
|
||||
import type { CastProtocol, ConnectionQuality } from "./types";
|
||||
|
||||
/**
|
||||
* Format milliseconds to HH:MM:SS or MM:SS
|
||||
*/
|
||||
@@ -46,19 +44,6 @@ export const calculateEndingTime = (
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine connection quality based on bitrate
|
||||
*/
|
||||
export const getConnectionQuality = (bitrate?: number): ConnectionQuality => {
|
||||
if (bitrate == null) return "good";
|
||||
const mbps = bitrate / 1000000;
|
||||
|
||||
if (mbps >= 15) return "excellent";
|
||||
if (mbps >= 8) return "good";
|
||||
if (mbps >= 4) return "fair";
|
||||
return "poor";
|
||||
};
|
||||
|
||||
/**
|
||||
* Get poster URL for item with specified dimensions
|
||||
*/
|
||||
@@ -103,78 +88,15 @@ export const isWithinSegment = (
|
||||
};
|
||||
|
||||
/**
|
||||
* Format bitrate to human-readable string
|
||||
* Format trickplay time from {hours, minutes, seconds} to display string.
|
||||
* Produces "H:MM:SS" when hours > 0, otherwise "MM:SS".
|
||||
*/
|
||||
export const formatBitrate = (bitrate: number): string => {
|
||||
const mbps = bitrate / 1000000;
|
||||
if (mbps >= 1) {
|
||||
return `${mbps.toFixed(1)} Mbps`;
|
||||
}
|
||||
return `${(bitrate / 1000).toFixed(0)} Kbps`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get protocol display name
|
||||
*/
|
||||
export const getProtocolName = (protocol: CastProtocol): string => {
|
||||
switch (protocol) {
|
||||
case "chromecast":
|
||||
return "Chromecast";
|
||||
default: {
|
||||
const _exhaustive: never = protocol;
|
||||
return String(_exhaustive);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get protocol icon name
|
||||
*/
|
||||
export const getProtocolIcon = (
|
||||
protocol: CastProtocol,
|
||||
): "tv" | "logo-apple" => {
|
||||
switch (protocol) {
|
||||
case "chromecast":
|
||||
return "tv";
|
||||
default: {
|
||||
const _exhaustive: never = protocol;
|
||||
return "tv";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Format episode info (e.g., "S1 E1" or "Episode 1")
|
||||
* @param seasonNumber - Season number
|
||||
* @param episodeNumber - Episode number
|
||||
* @param episodeLabel - Optional label for standalone episode (e.g. translated "Episode")
|
||||
*/
|
||||
export const formatEpisodeInfo = (
|
||||
seasonNumber?: number | null,
|
||||
episodeNumber?: number | null,
|
||||
episodeLabel = "Episode",
|
||||
): string => {
|
||||
if (
|
||||
seasonNumber !== undefined &&
|
||||
seasonNumber !== null &&
|
||||
episodeNumber !== undefined &&
|
||||
episodeNumber !== null
|
||||
) {
|
||||
return `S${seasonNumber} E${episodeNumber}`;
|
||||
}
|
||||
if (episodeNumber !== undefined && episodeNumber !== null) {
|
||||
return `${episodeLabel} ${episodeNumber}`;
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if we should show next episode countdown
|
||||
*/
|
||||
export const shouldShowNextEpisodeCountdown = (
|
||||
remainingMs: number,
|
||||
hasNextEpisode: boolean,
|
||||
countdownStartSeconds: number,
|
||||
): boolean => {
|
||||
return hasNextEpisode && remainingMs <= countdownStartSeconds * 1000;
|
||||
export const formatTrickplayTime = (time: {
|
||||
hours: number;
|
||||
minutes: number;
|
||||
seconds: number;
|
||||
}): string => {
|
||||
const mm = String(time.minutes).padStart(2, "0");
|
||||
const ss = String(time.seconds).padStart(2, "0");
|
||||
return time.hours > 0 ? `${time.hours}:${mm}:${ss}` : `${mm}:${ss}`;
|
||||
};
|
||||
|
||||
@@ -24,18 +24,9 @@ export interface CastPlayerState {
|
||||
progress: number;
|
||||
duration: number;
|
||||
volume: number;
|
||||
showControls: boolean;
|
||||
isBuffering: boolean;
|
||||
}
|
||||
|
||||
export interface CastSegmentData {
|
||||
intro: { start: number; end: number } | null;
|
||||
credits: { start: number; end: number } | null;
|
||||
recap: { start: number; end: number } | null;
|
||||
commercial: Array<{ start: number; end: number }>;
|
||||
preview: Array<{ start: number; end: number }>;
|
||||
}
|
||||
|
||||
export interface AudioTrack {
|
||||
index: number;
|
||||
language: string;
|
||||
@@ -77,8 +68,5 @@ export const DEFAULT_CAST_STATE: CastPlayerState = {
|
||||
progress: 0,
|
||||
duration: 0,
|
||||
volume: 0.5,
|
||||
showControls: true,
|
||||
isBuffering: false,
|
||||
};
|
||||
|
||||
export type ConnectionQuality = "excellent" | "good" | "fair" | "poor";
|
||||
|
||||
@@ -1,50 +1,7 @@
|
||||
/**
|
||||
* Chromecast player configuration and constants
|
||||
* Chromecast player configuration and types
|
||||
*/
|
||||
|
||||
export const CHROMECAST_CONSTANTS = {
|
||||
// Timing (all milliseconds for consistency)
|
||||
PROGRESS_REPORT_INTERVAL_MS: 10_000,
|
||||
CONTROLS_TIMEOUT_MS: 5_000,
|
||||
BUFFERING_THRESHOLD_MS: 10_000,
|
||||
NEXT_EPISODE_COUNTDOWN_MS: 30_000,
|
||||
CONNECTION_CHECK_INTERVAL_MS: 5_000,
|
||||
|
||||
// UI
|
||||
POSTER_WIDTH: 300,
|
||||
POSTER_HEIGHT: 450,
|
||||
MINI_PLAYER_HEIGHT: 80,
|
||||
SKIP_FORWARD_SECS: 15, // overridden by settings
|
||||
SKIP_BACKWARD_SECS: 15, // overridden by settings
|
||||
|
||||
// Animation
|
||||
ANIMATION_DURATION_MS: 300,
|
||||
BLUR_RADIUS: 10,
|
||||
} as const;
|
||||
|
||||
export const CONNECTION_QUALITY = {
|
||||
EXCELLENT: { min: 50, label: "Excellent", icon: "wifi" }, // min Mbps
|
||||
GOOD: { min: 30, label: "Good", icon: "signal" }, // min Mbps
|
||||
FAIR: { min: 15, label: "Fair", icon: "cellular" }, // min Mbps
|
||||
POOR: { min: 0, label: "Poor", icon: "warning" }, // min Mbps
|
||||
} as const;
|
||||
|
||||
export type ConnectionQuality = keyof typeof CONNECTION_QUALITY;
|
||||
|
||||
export type PlaybackState = "playing" | "paused" | "stopped" | "buffering";
|
||||
|
||||
export interface ChromecastPlayerState {
|
||||
isConnected: boolean;
|
||||
deviceName: string | null;
|
||||
playbackState: PlaybackState;
|
||||
progress: number; // milliseconds
|
||||
duration: number; // milliseconds
|
||||
volume: number; // 0-1
|
||||
isMuted: boolean;
|
||||
currentItemId: string | null;
|
||||
connectionQuality: ConnectionQuality;
|
||||
}
|
||||
|
||||
export interface ChromecastSegmentData {
|
||||
intro: { start: number; end: number } | null;
|
||||
credits: { start: number; end: number } | null;
|
||||
@@ -52,15 +9,3 @@ export interface ChromecastSegmentData {
|
||||
commercial: { start: number; end: number }[];
|
||||
preview: { start: number; end: number }[];
|
||||
}
|
||||
|
||||
export const DEFAULT_CHROMECAST_STATE: ChromecastPlayerState = {
|
||||
isConnected: false,
|
||||
deviceName: null,
|
||||
playbackState: "stopped",
|
||||
progress: 0,
|
||||
duration: 0,
|
||||
volume: 1,
|
||||
isMuted: false,
|
||||
currentItemId: null,
|
||||
connectionQuality: "GOOD",
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user