Skip to content

Commit cd31949

Browse files
Merge pull request #1 from ScopeCreep-zip/fix-multicast-issues
Fix multicast issues
2 parents b2af5a1 + 01337bc commit cd31949

9 files changed

Lines changed: 771 additions & 118 deletions

File tree

DEEPWIKI.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# DeepWiki Project Badge
22

3-
[![DeepWiki](https://mcp.deepwiki.com/badge.svg)](https://mcp.deepwiki.com/project?url=https://github.com/your-org/SpiritStream)
3+
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/ScopeCreep-zip/SpiritStream)
44

55
This badge enables DeepWiki scanning and documentation for this project.
66

src-frontend/lib/tauri.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ export const api = {
2424
stream: {
2525
start: (group: OutputGroup, incomingUrl: string) =>
2626
invoke<number>('start_stream', { group, incomingUrl }),
27+
startAll: (groups: OutputGroup[], incomingUrl: string) =>
28+
invoke<number[]>('start_all_streams', { groups, incomingUrl }),
2729
stop: (groupId: string) => invoke<void>('stop_stream', { groupId }),
2830
stopAll: () => invoke<void>('stop_all_streams'),
2931
getActiveCount: () => invoke<number>('get_active_stream_count'),

src-frontend/stores/streamStore.ts

Lines changed: 9 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,6 @@ const initialStats: StreamStats = {
7171
targetStats: {},
7272
};
7373

74-
const lastSamplesByGroup = new Map<string, { size: number; time: number }>();
75-
7674
export const useStreamStore = create<StreamState>((set, get) => ({
7775
isStreaming: false,
7876
activeGroups: new Set(),
@@ -95,10 +93,6 @@ export const useStreamStore = create<StreamState>((set, get) => ({
9593
const activeGroups = new Set(activeGroupIds);
9694
const isStreaming = activeCount > 0;
9795

98-
if (!isStreaming) {
99-
lastSamplesByGroup.clear();
100-
}
101-
10296
set({
10397
activeStreamCount: activeCount,
10498
activeGroups,
@@ -163,19 +157,18 @@ export const useStreamStore = create<StreamState>((set, get) => ({
163157
set({ globalStatus: 'connecting', error: null });
164158

165159
try {
166-
for (const group of groups) {
167-
// Skip groups with no targets at all
168-
if (group.streamTargets.length === 0) {
169-
continue;
170-
}
160+
const eligibleGroups = groups.filter((group) => group.streamTargets.length > 0);
161+
if (eligibleGroups.length === 0) {
162+
throw new Error('At least one stream target is required');
163+
}
171164

172-
// Send all targets - backend will filter disabled ones
173-
await api.stream.start(group, incomingUrl);
165+
await api.stream.startAll(eligibleGroups, incomingUrl);
174166

175-
const activeGroups = new Set(get().activeGroups);
167+
const activeGroups = new Set(get().activeGroups);
168+
for (const group of eligibleGroups) {
176169
activeGroups.add(group.id);
177-
set({ activeGroups });
178170
}
171+
set({ activeGroups });
179172

180173
set({
181174
isStreaming: true,
@@ -191,7 +184,6 @@ export const useStreamStore = create<StreamState>((set, get) => ({
191184
stopAllGroups: async () => {
192185
try {
193186
await api.stream.stopAll();
194-
lastSamplesByGroup.clear();
195187
set({
196188
activeGroups: new Set(),
197189
isStreaming: false,
@@ -265,23 +257,7 @@ export const useStreamStore = create<StreamState>((set, get) => ({
265257

266258
updateStats: (groupId, ffmpegStats) => {
267259
const currentGroupStats = get().groupStats;
268-
let bitrate = ffmpegStats.bitrate;
269-
const lastSample = lastSamplesByGroup.get(groupId);
270-
271-
if (
272-
lastSample &&
273-
ffmpegStats.time > lastSample.time &&
274-
ffmpegStats.size >= lastSample.size
275-
) {
276-
const deltaBytes = ffmpegStats.size - lastSample.size;
277-
const deltaSeconds = ffmpegStats.time - lastSample.time;
278-
const instantaneousKbps = (deltaBytes * 8) / 1000 / deltaSeconds;
279-
if (Number.isFinite(instantaneousKbps)) {
280-
bitrate = instantaneousKbps;
281-
}
282-
}
283-
284-
lastSamplesByGroup.set(groupId, { size: ffmpegStats.size, time: ffmpegStats.time });
260+
const bitrate = ffmpegStats.bitrate;
285261

286262
// Update per-group stats
287263
const newGroupStats = {
@@ -327,7 +303,6 @@ export const useStreamStore = create<StreamState>((set, get) => ({
327303
},
328304

329305
setStreamEnded: (groupId) => {
330-
lastSamplesByGroup.delete(groupId);
331306
const activeGroups = new Set(get().activeGroups);
332307
activeGroups.delete(groupId);
333308

@@ -356,7 +331,6 @@ export const useStreamStore = create<StreamState>((set, get) => ({
356331
},
357332

358333
setStreamError: (groupId, error) => {
359-
lastSamplesByGroup.delete(groupId);
360334
const activeGroups = new Set(get().activeGroups);
361335
activeGroups.delete(groupId);
362336

@@ -394,7 +368,6 @@ export const useStreamStore = create<StreamState>((set, get) => ({
394368
setError: (error) => set({ error }),
395369

396370
reset: () => {
397-
lastSamplesByGroup.clear();
398371
set({
399372
isStreaming: false,
400373
activeGroups: new Set(),

src-frontend/views/Dashboard.tsx

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState } from 'react';
1+
import { useMemo, useState } from 'react';
22
import { useTranslation } from 'react-i18next';
33
import {
44
Radio,
@@ -47,7 +47,8 @@ export function Dashboard({ onNavigate, onOpenProfileModal, onOpenTargetModal }:
4747
addOutputGroup,
4848
removeOutputGroup,
4949
} = useProfileStore();
50-
const { isStreaming, stats, uptime, globalStatus, activeStreamCount } = useStreamStore();
50+
const { isStreaming, stats, uptime, globalStatus, activeStreamCount, activeGroups, enabledTargets } =
51+
useStreamStore();
5152
const [isTesting, setIsTesting] = useState(false);
5253

5354
// Get output groups from current profile
@@ -152,9 +153,27 @@ export function Dashboard({ onNavigate, onOpenProfileModal, onOpenTargetModal }:
152153

153154
const targets = getAllTargets();
154155

156+
const activeTargetCount = useMemo(() => {
157+
if (!currentProfile) return 0;
158+
159+
return currentProfile.outputGroups.reduce((total, group) => {
160+
if (!activeGroups.has(group.id)) {
161+
return total;
162+
}
163+
const enabledCount = group.streamTargets.filter((t) => enabledTargets.has(t.id)).length;
164+
return total + enabledCount;
165+
}, 0);
166+
}, [currentProfile, activeGroups, enabledTargets]);
167+
155168
// Use backend-verified active stream count, fallback to local calculation
156169
const displayActiveCount =
157-
activeStreamCount > 0 ? activeStreamCount : isStreaming ? targets.length : 0;
170+
activeTargetCount > 0
171+
? activeTargetCount
172+
: activeStreamCount > 0
173+
? activeStreamCount
174+
: isStreaming
175+
? targets.length
176+
: 0;
158177

159178
if (loading) {
160179
return (

src-frontend/views/StreamManager.tsx

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { useEffect, useState, useMemo } from 'react';
22
import { useTranslation } from 'react-i18next';
3-
import { Play, Square, Settings2, Activity, Gauge, Clock, Upload } from 'lucide-react';
3+
import { Play, Square, Settings2, Activity, Gauge, Clock, Upload, Radio, AlertTriangle } from 'lucide-react';
44
import { Card, CardHeader, CardTitle, CardDescription, CardBody } from '@/components/ui/Card';
55
import { Button } from '@/components/ui/Button';
66
import { Alert } from '@/components/ui/Alert';
77
import { Toggle } from '@/components/ui/Toggle';
8+
import { StatsRow } from '@/components/dashboard/StatsRow';
9+
import { StatBox } from '@/components/dashboard/StatBox';
810
import { OutputGroup } from '@/components/stream/OutputGroup';
911
import { PlatformIcon } from '@/components/stream/PlatformIcon';
1012
import { useProfileStore } from '@/stores/profileStore';
@@ -29,6 +31,9 @@ export function StreamManager({ onNavigate }: StreamManagerProps) {
2931
globalStatus,
3032
groupStats,
3133
error: streamError,
34+
stats,
35+
uptime,
36+
activeStreamCount,
3237
startAllGroups,
3338
stopAllGroups,
3439
setTargetEnabled,
@@ -99,6 +104,20 @@ export function StreamManager({ onNavigate }: StreamManagerProps) {
99104
};
100105

101106
const isConnecting = globalStatus === 'connecting';
107+
const activeTargetCount = useMemo(() => {
108+
if (!current) return 0;
109+
110+
return current.outputGroups.reduce((total, group) => {
111+
if (!activeGroups.has(group.id)) {
112+
return total;
113+
}
114+
const enabledCount = group.streamTargets.filter((t) => enabledTargets.has(t.id)).length;
115+
return total + enabledCount;
116+
}, 0);
117+
}, [current, activeGroups, enabledTargets]);
118+
119+
const displayActiveCount =
120+
activeTargetCount > 0 ? activeTargetCount : activeStreamCount > 0 ? activeStreamCount : activeGroups.size;
102121

103122
// Calculate total bandwidth for enabled groups
104123
const totalBandwidth = useMemo(() => {
@@ -201,6 +220,35 @@ export function StreamManager({ onNavigate }: StreamManagerProps) {
201220
</Alert>
202221
)}
203222

223+
<StatsRow>
224+
<StatBox
225+
icon={<Radio className="w-5 h-5" />}
226+
label={t('dashboard.activeStreams')}
227+
value={displayActiveCount}
228+
change={isStreaming ? t('status.streaming') : t('status.readyToStart')}
229+
changeType={isStreaming ? 'positive' : 'neutral'}
230+
/>
231+
<StatBox
232+
icon={<Activity className="w-5 h-5" />}
233+
label={t('dashboard.totalBitrate')}
234+
value={stats.totalBitrate > 0 ? formatBitrate(stats.totalBitrate) : '0 kbps'}
235+
change={isStreaming ? t('status.active') : t('status.noActiveStreams')}
236+
/>
237+
<StatBox
238+
icon={<AlertTriangle className="w-5 h-5" />}
239+
label={t('dashboard.droppedFrames')}
240+
value={stats.droppedFrames}
241+
change={stats.droppedFrames === 0 ? t('status.noIssues') : t('status.checkConnection')}
242+
changeType={stats.droppedFrames === 0 ? 'positive' : 'neutral'}
243+
/>
244+
<StatBox
245+
icon={<Clock className="w-5 h-5" />}
246+
label={t('dashboard.uptime')}
247+
value={formatUptime(Math.floor(uptime))}
248+
change={isStreaming ? t('status.live') : t('status.notStreaming')}
249+
/>
250+
</StatsRow>
251+
204252
<Card>
205253
<CardHeader>
206254
<div>

src-tauri/src/commands/stream.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,34 @@ pub fn start_stream(
3030
ffmpeg_handler.start(&group, &incoming_url, &app)
3131
}
3232

33+
/// Start streaming for multiple output groups in one batch
34+
#[tauri::command]
35+
pub fn start_all_streams(
36+
app: AppHandle,
37+
groups: Vec<OutputGroup>,
38+
incoming_url: String,
39+
ffmpeg_handler: State<'_, FFmpegHandler>,
40+
) -> Result<Vec<u32>, String> {
41+
if incoming_url.is_empty() {
42+
return Err("Incoming URL is required".to_string());
43+
}
44+
45+
if groups.is_empty() {
46+
return Err("At least one output group is required".to_string());
47+
}
48+
49+
for group in &groups {
50+
if group.video.codec.is_empty() {
51+
return Err("Video encoder is required".to_string());
52+
}
53+
if group.audio.codec.is_empty() {
54+
return Err("Audio codec is required".to_string());
55+
}
56+
}
57+
58+
ffmpeg_handler.start_all(&groups, &incoming_url, &app)
59+
}
60+
3361
/// Stop streaming for an output group
3462
#[tauri::command]
3563
pub fn stop_stream(

src-tauri/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ pub fn run() {
105105
commands::validate_input,
106106
// Stream commands
107107
commands::start_stream,
108+
commands::start_all_streams,
108109
commands::stop_stream,
109110
commands::stop_all_streams,
110111
commands::get_active_stream_count,

0 commit comments

Comments
 (0)