tangled
alpha
login
or
join now
stream.place
/
streamplace
Live video on the AT Protocol
74
fork
atom
overview
issues
1
pulls
pipelines
bento updates + more accurate data
Natalie B.
5 months ago
3210fbf5
4cdc7150
+32
-204
5 changed files
expand all
collapse all
unified
split
js
app
components
live-dashboard
bento-grid.tsx
header.tsx
quick-stats.tsx
stream-controls.tsx
stream-monitor.tsx
-2
js/app/components/live-dashboard/bento-grid.tsx
···
4
import Header from "./header";
5
import LivestreamPanel from "./livestream-panel";
6
import ModActions from "./mod-actions";
7
-
import QuickStats from "./quick-stats";
8
import StreamMonitor from "./stream-monitor";
9
10
const { flex, p, gap, layout, bg } = zero;
···
53
<View style={[layout.flex.row, gap.all[4], flex.values[1]]}>
54
<ModActions isLive={isLive} />
55
</View>
56
-
<QuickStats />
57
</View>
58
59
<View
···
4
import Header from "./header";
5
import LivestreamPanel from "./livestream-panel";
6
import ModActions from "./mod-actions";
0
7
import StreamMonitor from "./stream-monitor";
8
9
const { flex, p, gap, layout, bg } = zero;
···
52
<View style={[layout.flex.row, gap.all[4], flex.values[1]]}>
53
<ModActions isLive={isLive} />
54
</View>
0
55
</View>
56
57
<View
+11
-40
js/app/components/live-dashboard/header.tsx
···
2
useLivestreamStore,
3
usePlayerStore,
4
useProfile,
0
5
zero,
6
} from "@streamplace/components";
7
-
import {
8
-
Activity,
9
-
Monitor,
10
-
Radio,
11
-
Signal,
12
-
Users,
13
-
Wifi,
14
-
} from "@tamagui/lucide-icons";
15
import { Text, View } from "react-native";
16
import { useLiveUser } from "../../hooks/useLiveUser";
17
import { useSegmentTiming } from "../../hooks/useSegmentTiming";
···
119
const isUserLive = useLiveUser();
120
const viewers = useLivestreamStore((x) => x.viewers);
121
const segmentTiming = useSegmentTiming();
0
122
const ingestConnectionState = usePlayerStore((x) => x.ingestConnectionState);
123
const ingestStarted = usePlayerStore((x) => x.ingestStarted);
124
···
151
}
152
};
153
154
-
const getFpsStatus = (fps: number): "good" | "warning" | "error" => {
155
-
if (fps >= 30) return "good";
156
-
if (fps >= 20) return "warning";
157
-
return "error";
158
-
};
159
-
160
-
const getBitrateStatus = (bitrate: string): "good" | "warning" | "error" => {
161
-
const value = parseInt(bitrate);
162
-
if (value >= 2000) return "good";
163
-
if (value >= 1000) return "warning";
164
-
return "error";
165
-
};
166
167
return (
168
<View
···
198
/>
199
<MetricItem
200
icon={Activity}
201
-
label="Segments"
202
value={`${segmentTiming.timeBetweenSegments || 0}ms`}
203
status={
204
segmentTiming.connectionQuality === "good"
···
209
}
210
/>
211
<MetricItem
212
-
icon={Monitor}
213
-
label="Quality"
214
-
value={segmentTiming.connectionQuality.toUpperCase()}
215
-
status={
216
-
segmentTiming.connectionQuality === "good"
217
-
? "good"
218
-
: segmentTiming.connectionQuality === "degraded"
219
-
? "warning"
220
-
: "error"
221
}
222
-
/>
223
-
<MetricItem
224
-
icon={Radio}
225
-
label="Connection"
226
-
value={ingestConnectionState || "disconnected"}
227
-
/>
228
-
<MetricItem
229
-
icon={Wifi}
230
-
label="Range"
231
-
value={segmentTiming.range ? `${segmentTiming.range}ms` : "N/A"}
232
/>
233
<MetricItem icon={Signal} label="Uptime" value={getUptime()} />
234
</>
···
2
useLivestreamStore,
3
usePlayerStore,
4
useProfile,
5
+
useSegment,
6
zero,
7
} from "@streamplace/components";
8
+
import { Activity, Car, Radio, Signal, Users } from "@tamagui/lucide-icons";
0
0
0
0
0
0
0
9
import { Text, View } from "react-native";
10
import { useLiveUser } from "../../hooks/useLiveUser";
11
import { useSegmentTiming } from "../../hooks/useSegmentTiming";
···
113
const isUserLive = useLiveUser();
114
const viewers = useLivestreamStore((x) => x.viewers);
115
const segmentTiming = useSegmentTiming();
116
+
const seg = useSegment();
117
const ingestConnectionState = usePlayerStore((x) => x.ingestConnectionState);
118
const ingestStarted = usePlayerStore((x) => x.ingestStarted);
119
···
146
}
147
};
148
149
+
console.log(seg);
0
0
0
0
0
0
0
0
0
0
0
150
151
return (
152
<View
···
182
/>
183
<MetricItem
184
icon={Activity}
185
+
label="Time between Segments"
186
value={`${segmentTiming.timeBetweenSegments || 0}ms`}
187
status={
188
segmentTiming.connectionQuality === "good"
···
193
}
194
/>
195
<MetricItem
196
+
icon={Car}
197
+
label="Bitrate"
198
+
value={
199
+
seg?.size
200
+
? `${((seg.size * 8) / ((seg.duration || 1000000000) / 1000000000) / 1000 / 1000).toFixed(2)} kbps`
201
+
: "0 kbps"
0
0
0
202
}
0
0
0
0
0
0
0
0
0
0
203
/>
204
<MetricItem icon={Signal} label="Uptime" value={getUptime()} />
205
</>
-85
js/app/components/live-dashboard/quick-stats.tsx
···
1
-
import { useLivestreamStore, zero } from "@streamplace/components";
2
-
import { Heart, MessageCircle, Users } from "@tamagui/lucide-icons";
3
-
import { Text, View } from "react-native";
4
-
import { useSegmentTiming } from "../../hooks/useSegmentTiming";
5
-
6
-
const { flex, bg, r, p, text, layout, gap, borderRadius } = zero;
7
-
8
-
interface StatCardProps {
9
-
icon: any;
10
-
label: string;
11
-
value: string;
12
-
color: "blue" | "red" | "green";
13
-
}
14
-
15
-
function StatCard({ icon: Icon, label, value, color }: StatCardProps) {
16
-
const colors = {
17
-
blue: { bg: bg.blue[500], text: text.blue[100] },
18
-
red: { bg: bg.red[500], text: text.red[100] },
19
-
green: { bg: bg.green[500], text: text.green[100] },
20
-
};
21
-
22
-
return (
23
-
<View
24
-
style={[
25
-
flex.values[1],
26
-
colors[color].bg,
27
-
r[3],
28
-
p[4],
29
-
layout.flex.row,
30
-
layout.flex.alignCenter,
31
-
gap.all[3],
32
-
borderRadius["2xl"],
33
-
]}
34
-
>
35
-
<Icon size={24} color="white" />
36
-
<View>
37
-
<Text style={[text.white, { fontSize: 24, fontWeight: "700" }]}>
38
-
{value}
39
-
</Text>
40
-
<Text style={[colors[color].text, { fontSize: 12, fontWeight: "500" }]}>
41
-
{label}
42
-
</Text>
43
-
</View>
44
-
</View>
45
-
);
46
-
}
47
-
48
-
export default function QuickStats() {
49
-
// Get real data from stores
50
-
const viewers = useLivestreamStore((x) => x.viewers);
51
-
const chat = useLivestreamStore((x) => x.chat);
52
-
const segmentTiming = useSegmentTiming();
53
-
54
-
// Calculate stats from real data
55
-
const viewerCount = viewers || 0;
56
-
const messageCount = chat?.length || 0;
57
-
58
-
// Count likes/hearts in chat messages (simplified - could be more sophisticated)
59
-
const likeCount = 0;
60
-
61
-
return (
62
-
<View
63
-
style={[flex.values[1], gap.all[4], layout.flex.row, { maxHeight: 64 }]}
64
-
>
65
-
<StatCard
66
-
icon={Users}
67
-
label="Viewers"
68
-
value={viewerCount.toLocaleString()}
69
-
color="blue"
70
-
/>
71
-
<StatCard
72
-
icon={Heart}
73
-
label="Likes"
74
-
value={likeCount.toString()}
75
-
color="red"
76
-
/>
77
-
<StatCard
78
-
icon={MessageCircle}
79
-
label="Messages"
80
-
value={messageCount.toString()}
81
-
color="green"
82
-
/>
83
-
</View>
84
-
);
85
-
}
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
-57
js/app/components/live-dashboard/stream-controls.tsx
···
1
-
import { zero } from "@streamplace/components";
2
-
import { Play, Square } from "@tamagui/lucide-icons";
3
-
import { Pressable, Text, View } from "react-native";
4
-
5
-
const { flex, bg, r, borders, p, px, py, text, layout, gap } = zero;
6
-
7
-
// Mock data - replace with real data from your stores
8
-
const mockStats = {
9
-
uptime: "02:34:12",
10
-
};
11
-
12
-
interface StreamControlsProps {
13
-
isLive: boolean;
14
-
}
15
-
16
-
export default function StreamControls({ isLive }: StreamControlsProps) {
17
-
return (
18
-
<View
19
-
style={[
20
-
flex.values[1],
21
-
bg.gray[800],
22
-
r[3],
23
-
borders.width.thin,
24
-
borders.color.gray[700],
25
-
p[4],
26
-
layout.flex.row,
27
-
layout.flex.alignCenter,
28
-
gap.all[3],
29
-
]}
30
-
>
31
-
<Pressable
32
-
style={[
33
-
bg[isLive ? "red" : "green"][500],
34
-
r[2],
35
-
px[4],
36
-
py[3],
37
-
layout.flex.row,
38
-
layout.flex.alignCenter,
39
-
gap.all[2],
40
-
]}
41
-
>
42
-
{isLive ? (
43
-
<Square size={20} color="white" />
44
-
) : (
45
-
<Play size={20} color="white" />
46
-
)}
47
-
<Text style={[text.white, { fontSize: 16, fontWeight: "600" }]}>
48
-
{isLive ? "Stop Stream" : "Go Live"}
49
-
</Text>
50
-
</Pressable>
51
-
52
-
<Text style={[text.gray[400], { fontSize: 14 }]}>
53
-
Uptime: {mockStats.uptime}
54
-
</Text>
55
-
</View>
56
-
);
57
-
}
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
+21
-20
js/app/components/live-dashboard/stream-monitor.tsx
···
1
import {
2
Player,
0
3
useLivestreamStore,
4
usePlayerStore,
5
zero,
···
26
const isUserLive = useLiveUser();
27
const profile = useLivestreamStore((x) => x.profile);
28
const ingestConnectionState = usePlayerStore((x) => x.ingestConnectionState);
0
29
const segmentTiming = useSegmentTiming();
30
31
// Use hook data primarily, fallback to props
···
73
layout.flex.column,
74
]}
75
>
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
76
<View
77
style={[
78
layout.flex.row,
···
84
]}
85
>
86
<Text style={[text.white, { fontSize: 18, fontWeight: "600" }]}>
87
-
Stream Monitor
88
</Text>
89
-
<View style={[layout.flex.row, layout.flex.alignCenter, { gap: 8 }]}>
90
{getConnectionIcon()}
91
<View style={[w[2], h[2], r[1], bg[getConnectionColor()][500]]} />
92
<Text style={[text.gray[400], { fontSize: 14 }]}>
···
98
</Text>
99
)}
100
</View>
101
-
</View>
102
-
103
-
<View style={[flex.values[1], layout.flex.center, bg.gray[900]]}>
104
-
{isLive && userProfile ? (
105
-
<Player src={userProfile.did} name={userProfile.handle} />
106
-
) : (
107
-
<View style={[layout.flex.center, { gap: 12 }]}>
108
-
<Camera size={48} color="#6b7280" />
109
-
<Text style={[text.gray[400]]}>
110
-
{!userProfile ? "No Profile" : "Stream Offline"}
111
-
</Text>
112
-
{ingestConnectionState && (
113
-
<Text style={[text.gray[500], { fontSize: 12 }]}>
114
-
Connection: {ingestConnectionState}
115
-
</Text>
116
-
)}
117
-
</View>
118
-
)}
119
</View>
120
</View>
121
);
···
1
import {
2
Player,
3
+
useLivestream,
4
useLivestreamStore,
5
usePlayerStore,
6
zero,
···
27
const isUserLive = useLiveUser();
28
const profile = useLivestreamStore((x) => x.profile);
29
const ingestConnectionState = usePlayerStore((x) => x.ingestConnectionState);
30
+
const ls = useLivestream();
31
const segmentTiming = useSegmentTiming();
32
33
// Use hook data primarily, fallback to props
···
75
layout.flex.column,
76
]}
77
>
78
+
<View style={[flex.values[1], layout.flex.center, bg.gray[900]]}>
79
+
{isLive && userProfile ? (
80
+
<Player src={userProfile.did} name={userProfile.handle} />
81
+
) : (
82
+
<View style={[layout.flex.center, { gap: 12 }]}>
83
+
<Camera size={48} color="#6b7280" />
84
+
<Text style={[text.gray[400]]}>
85
+
{!userProfile ? "No Profile" : "Stream Offline"}
86
+
</Text>
87
+
{ingestConnectionState && (
88
+
<Text style={[text.gray[500], { fontSize: 12 }]}>
89
+
Connection: {ingestConnectionState}
90
+
</Text>
91
+
)}
92
+
</View>
93
+
)}
94
+
</View>
95
<View
96
style={[
97
layout.flex.row,
···
103
]}
104
>
105
<Text style={[text.white, { fontSize: 18, fontWeight: "600" }]}>
106
+
{ls?.record.title || "Stream Title"}
107
</Text>
108
+
<View style={[layout.flex.row, layout.flex.center, { gap: 8 }]}>
109
{getConnectionIcon()}
110
<View style={[w[2], h[2], r[1], bg[getConnectionColor()][500]]} />
111
<Text style={[text.gray[400], { fontSize: 14 }]}>
···
117
</Text>
118
)}
119
</View>
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
120
</View>
121
</View>
122
);