diff --git a/frontend/src/api.js b/frontend/src/api.js index 5c9e415d..b0e2c5d9 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -1238,4 +1238,18 @@ export default class API { errorNotification(`Failed to delete recording ${id}`, e); } } + + static async switchStream(channelId, streamId) { + try { + const response = await request(`${host}/proxy/ts/next_stream/${channelId}`, { + method: 'POST', + body: { stream_id: streamId }, + }); + + return response; + } catch (e) { + errorNotification('Failed to switch stream', e); + throw e; + } + } } diff --git a/frontend/src/pages/Stats.jsx b/frontend/src/pages/Stats.jsx index 3f2fbb24..0709712e 100644 --- a/frontend/src/pages/Stats.jsx +++ b/frontend/src/pages/Stats.jsx @@ -13,6 +13,7 @@ import { Title, Tooltip, useMantineTheme, + Select, } from '@mantine/core'; import { MantineReactTable, useMantineReactTable } from 'mantine-react-table'; import { TableHelper } from '../helpers'; @@ -34,6 +35,7 @@ import relativeTime from 'dayjs/plugin/relativeTime'; import { Sparkline } from '@mantine/charts'; import useStreamProfilesStore from '../store/streamProfiles'; import { useLocation } from 'react-router-dom'; +import { notifications } from '@mantine/notifications'; dayjs.extend(duration); dayjs.extend(relativeTime); @@ -75,14 +77,55 @@ const getStartDate = (uptime) => { }; // Create a separate component for each channel card to properly handle the hook -const ChannelCard = ({ channel, clients, stopClient, stopChannel, logos }) => { +const ChannelCard = ({ channel, clients, stopClient, stopChannel, logos, channelsByUUID }) => { const location = useLocation(); + const [availableStreams, setAvailableStreams] = useState([]); + const [isLoadingStreams, setIsLoadingStreams] = useState(false); // Safety check - if channel doesn't have required data, don't render if (!channel || !channel.channel_id) { return null; } + // Fetch available streams for this channel + useEffect(() => { + const fetchStreams = async () => { + setIsLoadingStreams(true); + try { + // Get channel ID from UUID + const channelId = channelsByUUID[channel.channel_id]; + if (channelId) { + const streamData = await API.getChannelStreams(channelId); + setAvailableStreams(streamData); + } + } catch (error) { + console.error("Error fetching streams:", error); + } finally { + setIsLoadingStreams(false); + } + }; + + fetchStreams(); + }, [channel.channel_id, channelsByUUID]); + + // Handle stream switching + const handleStreamChange = async (streamId) => { + try { + await API.switchStream(channel.channel_id, streamId); + notifications.show({ + title: 'Stream switching', + message: `Switching stream for ${channel.name}`, + color: 'blue.5', + }); + } catch (error) { + notifications.show({ + title: 'Error switching stream', + message: error.toString(), + color: 'red.5', + }); + } + }; + const clientsColumns = useMemo( () => [ { @@ -167,6 +210,12 @@ const ChannelCard = ({ channel, clients, stopClient, stopChannel, logos }) => { const avgBitrate = channel.avg_bitrate || '0 Kbps'; const streamProfileName = channel.stream_profile?.name || 'Unknown Profile'; + // Create select options for available streams + const streamOptions = availableStreams.map(stream => ({ + value: stream.id.toString(), + label: stream.name || `Stream #${stream.id}` + })); + return ( { + {/* Add stream selection dropdown */} + {availableStreams.length > 0 && ( +