diff --git a/frontend/src/components/tables/ChannelTableStreams.jsx b/frontend/src/components/tables/ChannelTableStreams.jsx
index 391f4d30..373427bb 100644
--- a/frontend/src/components/tables/ChannelTableStreams.jsx
+++ b/frontend/src/components/tables/ChannelTableStreams.jsx
@@ -1,7 +1,7 @@
import React, { useMemo, useState, useEffect } from 'react';
import API from '../../api';
import { copyToClipboard } from '../../utils';
-import { GripHorizontal, SquareMinus } from 'lucide-react';
+import { GripHorizontal, SquareMinus, ChevronDown, ChevronRight } from 'lucide-react';
import {
Box,
ActionIcon,
@@ -12,6 +12,8 @@ import {
Badge,
Group,
Tooltip,
+ Collapse,
+ Button,
} from '@mantine/core';
import { notifications } from '@mantine/notifications';
import {
@@ -159,6 +161,122 @@ const ChannelStreams = ({ channel, isExpanded }) => {
return map;
}, [playlists]);
+ // Add state for tracking which streams have advanced stats expanded
+ const [expandedAdvancedStats, setExpandedAdvancedStats] = useState(new Set());
+
+ // Helper function to categorize stream stats
+ const categorizeStreamStats = (stats) => {
+ if (!stats) return { basic: {}, video: {}, audio: {}, technical: {}, other: {} };
+
+ const categories = {
+ basic: {},
+ video: {},
+ audio: {},
+ technical: {},
+ other: {}
+ };
+
+ // Define which stats go in which category
+ const categoryMapping = {
+ basic: ['resolution', 'video_codec', 'source_fps', 'audio_codec', 'audio_channels'],
+ video: ['video_bitrate', 'pixel_format', 'width', 'height', 'aspect_ratio', 'frame_rate'],
+ audio: ['audio_bitrate', 'sample_rate', 'audio_format', 'audio_channels_layout'],
+ technical: ['stream_type', 'container_format', 'duration', 'file_size', 'ffmpeg_output_bitrate'],
+ other: [] // Will catch anything not categorized above
+ };
+
+ // Categorize each stat
+ Object.entries(stats).forEach(([key, value]) => {
+ let categorized = false;
+
+ for (const [category, keys] of Object.entries(categoryMapping)) {
+ if (keys.includes(key)) {
+ categories[category][key] = value;
+ categorized = true;
+ break;
+ }
+ }
+
+ // If not categorized, put it in 'other'
+ if (!categorized) {
+ categories.other[key] = value;
+ }
+ });
+
+ return categories;
+ };
+
+ // Function to format stat values for display
+ const formatStatValue = (key, value) => {
+ if (value === null || value === undefined) return 'N/A';
+
+ // Handle specific formatting cases
+ switch (key) {
+ case 'video_bitrate':
+ case 'audio_bitrate':
+ case 'ffmpeg_output_bitrate':
+ return `${value} kbps`;
+ case 'source_fps':
+ case 'frame_rate':
+ return `${value} fps`;
+ case 'sample_rate':
+ return `${value} Hz`;
+ case 'file_size':
+ // Convert bytes to appropriate unit
+ if (typeof value === 'number') {
+ if (value < 1024) return `${value} B`;
+ if (value < 1024 * 1024) return `${(value / 1024).toFixed(2)} KB`;
+ if (value < 1024 * 1024 * 1024) return `${(value / (1024 * 1024)).toFixed(2)} MB`;
+ return `${(value / (1024 * 1024 * 1024)).toFixed(2)} GB`;
+ }
+ return value;
+ case 'duration':
+ // Format duration if it's in seconds
+ if (typeof value === 'number') {
+ const hours = Math.floor(value / 3600);
+ const minutes = Math.floor((value % 3600) / 60);
+ const seconds = Math.floor(value % 60);
+ return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
+ }
+ return value;
+ default:
+ return value.toString();
+ }
+ };
+
+ // Function to render a stats category
+ const renderStatsCategory = (categoryName, stats) => {
+ if (!stats || Object.keys(stats).length === 0) return null;
+
+ return (
+
+
+ {categoryName}
+
+
+ {Object.entries(stats).map(([key, value]) => (
+
+
+ {key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}: {formatStatValue(key, value)}
+
+
+ ))}
+
+
+ );
+ };
+
+ // Function to toggle advanced stats for a stream
+ const toggleAdvancedStats = (streamId) => {
+ const newExpanded = new Set(expandedAdvancedStats);
+ if (newExpanded.has(streamId)) {
+ newExpanded.delete(streamId);
+ } else {
+ newExpanded.add(streamId);
+ }
+ setExpandedAdvancedStats(newExpanded);
+ };
+
const table = useReactTable({
columns: useMemo(
() => [
@@ -177,6 +295,12 @@ const ChannelStreams = ({ channel, isExpanded }) => {
const playlistName = playlists[stream.m3u_account]?.name || 'Unknown';
const accountName = m3uAccountsMap[stream.m3u_account] || playlistName;
+ // Categorize stream stats
+ const categorizedStats = categorizeStreamStats(stream.stream_stats);
+ const hasAdvancedStats = Object.values(categorizedStats).some(category =>
+ Object.keys(category).length > 0
+ );
+
return (
{stream.name}
@@ -211,6 +335,8 @@ const ChannelStreams = ({ channel, isExpanded }) => {
)}
+
+ {/* Basic Stream Stats (always shown) */}
{stream.stream_stats && (
{/* Video Information */}
@@ -224,7 +350,7 @@ const ChannelStreams = ({ channel, isExpanded }) => {
)}
{stream.stream_stats.video_bitrate && (
- {stream.stream_stats.video_bitrate} KbPS
+ {stream.stream_stats.video_bitrate} kbps
)}
{stream.stream_stats.source_fps && (
@@ -256,7 +382,7 @@ const ChannelStreams = ({ channel, isExpanded }) => {
)}
{stream.stream_stats.audio_bitrate && (
- {stream.stream_stats.audio_bitrate} KbPS
+ {stream.stream_stats.audio_bitrate} kbps
)}
>
@@ -268,13 +394,45 @@ const ChannelStreams = ({ channel, isExpanded }) => {
Output Bitrate:
{stream.stream_stats.ffmpeg_output_bitrate && (
- {stream.stream_stats.ffmpeg_output_bitrate} KbPS
+ {stream.stream_stats.ffmpeg_output_bitrate} kbps
)}
>
)}
)}
+
+ {/* Advanced Stats Toggle Button */}
+ {hasAdvancedStats && (
+
+ : }
+ onClick={() => toggleAdvancedStats(stream.id)}
+ c="dimmed"
+ >
+ {expandedAdvancedStats.has(stream.id) ? 'Hide' : 'Show'} Advanced Stats
+
+
+ )}
+
+ {/* Advanced Stats (expandable) */}
+
+
+ {renderStatsCategory('Video', categorizedStats.video)}
+ {renderStatsCategory('Audio', categorizedStats.audio)}
+ {renderStatsCategory('Technical', categorizedStats.technical)}
+ {renderStatsCategory('Other', categorizedStats.other)}
+
+ {/* Show when stats were last updated */}
+ {stream.stream_stats_updated_at && (
+
+ Last updated: {new Date(stream.stream_stats_updated_at).toLocaleString()}
+
+ )}
+
+
);
},
@@ -296,7 +454,7 @@ const ChannelStreams = ({ channel, isExpanded }) => {
),
},
],
- [data, playlists, m3uAccountsMap]
+ [data, playlists, m3uAccountsMap, expandedAdvancedStats]
),
data,
state: {