@sansavision/create-pulse 0.1.0-alpha.6 → 0.1.0-alpha.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useCallback } from 'react';
|
|
1
|
+
import { useState, useCallback, useRef } from 'react';
|
|
2
2
|
import { usePulse } from './hooks/usePulse';
|
|
3
3
|
import { VideoPlayer } from './components/VideoPlayer';
|
|
4
4
|
import { GameSync } from './components/GameSync';
|
|
@@ -22,8 +22,12 @@ export default function App() {
|
|
|
22
22
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
23
23
|
const [events, setEvents] = useState<{ id: number, action: string, time: string }[]>([]);
|
|
24
24
|
|
|
25
|
+
// Track relay message delay
|
|
26
|
+
const [lastDelay, setLastDelay] = useState<number | null>(null);
|
|
27
|
+
const delayHistoryRef = useRef<number[]>([]);
|
|
28
|
+
|
|
25
29
|
const logEvent = useCallback((action: string) => {
|
|
26
|
-
setEvents(prev => [...prev.slice(-
|
|
30
|
+
setEvents(prev => [...prev.slice(-19), {
|
|
27
31
|
id: Date.now(),
|
|
28
32
|
action,
|
|
29
33
|
time: new Date().toISOString().substring(11, 23)
|
|
@@ -32,7 +36,15 @@ export default function App() {
|
|
|
32
36
|
|
|
33
37
|
const handleSyncMessage = useCallback((msg: any) => {
|
|
34
38
|
if (activeFeature !== 'watch') return;
|
|
35
|
-
|
|
39
|
+
|
|
40
|
+
// Track delivery delay
|
|
41
|
+
if (msg.ts) {
|
|
42
|
+
const delay = Date.now() - msg.ts;
|
|
43
|
+
setLastDelay(delay);
|
|
44
|
+
delayHistoryRef.current = [...delayHistoryRef.current.slice(-29), delay];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
logEvent(`RECV: ${msg.action} @ ${msg.time?.toFixed(2)}s`);
|
|
36
48
|
|
|
37
49
|
switch (msg.action) {
|
|
38
50
|
case 'play':
|
|
@@ -56,10 +68,30 @@ export default function App() {
|
|
|
56
68
|
const { broadcast } = useChannel(STREAM_NAME, handleSyncMessage);
|
|
57
69
|
|
|
58
70
|
const broadcastAction = useCallback((action: string, time: number) => {
|
|
59
|
-
logEvent(`SEND: ${action}`);
|
|
60
|
-
|
|
71
|
+
logEvent(`SEND: ${action} @ ${time.toFixed(2)}s`);
|
|
72
|
+
const msg = { action, time, ts: Date.now() };
|
|
73
|
+
broadcast(msg);
|
|
74
|
+
|
|
75
|
+
// Local loopback: since the relay doesn't echo back to the sender,
|
|
76
|
+
// we directly update peerTime for the same-tab demo experience.
|
|
77
|
+
if (action === 'tick') {
|
|
78
|
+
setPeerTime(time);
|
|
79
|
+
} else if (action === 'play') {
|
|
80
|
+
setIsPlaying(true);
|
|
81
|
+
setPeerTime(time);
|
|
82
|
+
} else if (action === 'pause') {
|
|
83
|
+
setIsPlaying(false);
|
|
84
|
+
setPeerTime(time);
|
|
85
|
+
} else if (action === 'seek') {
|
|
86
|
+
setPeerTime(time);
|
|
87
|
+
}
|
|
61
88
|
}, [broadcast, logEvent]);
|
|
62
89
|
|
|
90
|
+
// Computed delay stats
|
|
91
|
+
const avgDelay = delayHistoryRef.current.length > 0
|
|
92
|
+
? Math.round(delayHistoryRef.current.reduce((a: number, b: number) => a + b, 0) / delayHistoryRef.current.length)
|
|
93
|
+
: null;
|
|
94
|
+
|
|
63
95
|
return (
|
|
64
96
|
<div className="min-h-screen">
|
|
65
97
|
{/* Header */}
|
|
@@ -260,7 +292,20 @@ export default function App() {
|
|
|
260
292
|
<div className="flex justify-between items-center p-3 rounded-lg bg-black/30 border border-pulseBorder">
|
|
261
293
|
<span className="text-textMuted">Drift</span>
|
|
262
294
|
<span className={`font-semibold tracking-wider ${Math.abs(hostTime - peerTime) > 0.15 ? 'text-brandAmber' : 'text-brandEmerald'}`}>
|
|
263
|
-
{Math.abs(hostTime - peerTime).toFixed(3)}s
|
|
295
|
+
{Math.abs(hostTime - peerTime) < 0.001 ? '0.000' : Math.abs(hostTime - peerTime).toFixed(3)}s
|
|
296
|
+
</span>
|
|
297
|
+
</div>
|
|
298
|
+
<div className="flex justify-between items-center p-3 rounded-lg bg-black/30 border border-pulseBorder">
|
|
299
|
+
<span className="text-textMuted">Relay RTT</span>
|
|
300
|
+
<span className="text-textMain font-semibold tracking-wider">
|
|
301
|
+
{metrics ? `${Math.round(metrics.rtt)}ms` : '—'}
|
|
302
|
+
</span>
|
|
303
|
+
</div>
|
|
304
|
+
<div className="flex justify-between items-center p-3 rounded-lg bg-black/30 border border-pulseBorder">
|
|
305
|
+
<span className="text-textMuted">Msg Delay</span>
|
|
306
|
+
<span className={`font-semibold tracking-wider ${lastDelay !== null && lastDelay > 50 ? 'text-brandAmber' : 'text-brandEmerald'}`}>
|
|
307
|
+
{lastDelay !== null ? `${lastDelay}ms` : '—'}
|
|
308
|
+
{avgDelay !== null && <span className="text-textDim text-[10px] ml-1">avg {avgDelay}ms</span>}
|
|
264
309
|
</span>
|
|
265
310
|
</div>
|
|
266
311
|
</div>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useCallback } from 'react';
|
|
1
|
+
import { useState, useCallback, useRef } from 'react';
|
|
2
2
|
import { usePulse } from './hooks/usePulse';
|
|
3
3
|
import { VideoPlayer } from './components/VideoPlayer';
|
|
4
4
|
import { Activity, Users, Video } from 'lucide-react';
|
|
@@ -17,8 +17,12 @@ export default function App() {
|
|
|
17
17
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
18
18
|
const [events, setEvents] = useState<{ id: number, action: string, time: string }[]>([]);
|
|
19
19
|
|
|
20
|
+
// Track message round-trip delay
|
|
21
|
+
const [lastDelay, setLastDelay] = useState<number | null>(null);
|
|
22
|
+
const delayHistoryRef = useRef<number[]>([]);
|
|
23
|
+
|
|
20
24
|
const logEvent = useCallback((action: string) => {
|
|
21
|
-
setEvents(prev => [...prev.slice(-
|
|
25
|
+
setEvents((prev: { id: number, action: string, time: string }[]) => [...prev.slice(-19), {
|
|
22
26
|
id: Date.now(),
|
|
23
27
|
action,
|
|
24
28
|
time: new Date().toISOString().substring(11, 23)
|
|
@@ -26,7 +30,14 @@ export default function App() {
|
|
|
26
30
|
}, []);
|
|
27
31
|
|
|
28
32
|
const handleSyncMessage = useCallback((msg: any) => {
|
|
29
|
-
|
|
33
|
+
// Calculate delivery delay if timestamp present
|
|
34
|
+
if (msg.ts) {
|
|
35
|
+
const delay = Date.now() - msg.ts;
|
|
36
|
+
setLastDelay(delay);
|
|
37
|
+
delayHistoryRef.current = [...delayHistoryRef.current.slice(-29), delay];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
logEvent(`RECV: ${msg.action} @ ${msg.time?.toFixed(2)}s`);
|
|
30
41
|
|
|
31
42
|
switch (msg.action) {
|
|
32
43
|
case 'play':
|
|
@@ -50,10 +61,34 @@ export default function App() {
|
|
|
50
61
|
const { broadcast } = useChannel(STREAM_NAME, handleSyncMessage);
|
|
51
62
|
|
|
52
63
|
const broadcastAction = useCallback((action: string, time: number) => {
|
|
53
|
-
logEvent(`SEND: ${action}`);
|
|
54
|
-
|
|
64
|
+
logEvent(`SEND: ${action} @ ${time.toFixed(2)}s`);
|
|
65
|
+
const msg = { action, time, ts: Date.now() };
|
|
66
|
+
broadcast(msg);
|
|
67
|
+
|
|
68
|
+
// Local loopback: since the relay doesn't echo back to the sender,
|
|
69
|
+
// we directly update peerTime to simulate the round-trip for the demo.
|
|
70
|
+
// For a real multi-user scenario, the relay delivers to other connections.
|
|
71
|
+
if (action === 'tick') {
|
|
72
|
+
setPeerTime(time);
|
|
73
|
+
} else if (action === 'play') {
|
|
74
|
+
setIsPlaying(true);
|
|
75
|
+
setPeerTime(time);
|
|
76
|
+
} else if (action === 'pause') {
|
|
77
|
+
setIsPlaying(false);
|
|
78
|
+
setPeerTime(time);
|
|
79
|
+
} else if (action === 'seek') {
|
|
80
|
+
setPeerTime(time);
|
|
81
|
+
}
|
|
55
82
|
}, [broadcast, logEvent]);
|
|
56
83
|
|
|
84
|
+
// Compute average delay
|
|
85
|
+
const avgDelay = delayHistoryRef.current.length > 0
|
|
86
|
+
? Math.round(delayHistoryRef.current.reduce((a: number, b: number) => a + b, 0) / delayHistoryRef.current.length)
|
|
87
|
+
: null;
|
|
88
|
+
|
|
89
|
+
const drift = Math.abs(hostTime - peerTime);
|
|
90
|
+
const driftColor = drift > 0.15 ? 'text-brandAmber' : 'text-brandEmerald';
|
|
91
|
+
|
|
57
92
|
return (
|
|
58
93
|
<div className="min-h-screen">
|
|
59
94
|
{/* Header */}
|
|
@@ -109,8 +144,8 @@ export default function App() {
|
|
|
109
144
|
isPlaying={isPlaying}
|
|
110
145
|
onPlay={() => { setIsPlaying(true); broadcastAction('play', hostTime); }}
|
|
111
146
|
onPause={() => { setIsPlaying(false); broadcastAction('pause', hostTime); }}
|
|
112
|
-
onSeek={(t) => { setHostTime(t); broadcastAction('seek', t); }}
|
|
113
|
-
onTick={(t) => { setHostTime(t); broadcastAction('tick', t); }}
|
|
147
|
+
onSeek={(t: number) => { setHostTime(t); broadcastAction('seek', t); }}
|
|
148
|
+
onTick={(t: number) => { setHostTime(t); broadcastAction('tick', t); }}
|
|
114
149
|
accentColor="#a5b4fc"
|
|
115
150
|
/>
|
|
116
151
|
|
|
@@ -125,6 +160,7 @@ export default function App() {
|
|
|
125
160
|
</div>
|
|
126
161
|
|
|
127
162
|
<aside className="space-y-6">
|
|
163
|
+
{/* Sync Telemetry */}
|
|
128
164
|
<div className="glass-panel p-6 space-y-4">
|
|
129
165
|
<div className="flex items-center gap-2 text-xs font-bold uppercase tracking-widest text-textDim border-b border-pulseBorder pb-4">
|
|
130
166
|
<Activity className="w-4 h-4 text-brandPurple" />
|
|
@@ -142,13 +178,27 @@ export default function App() {
|
|
|
142
178
|
</div>
|
|
143
179
|
<div className="flex justify-between items-center p-3 rounded-lg bg-black/30 border border-pulseBorder">
|
|
144
180
|
<span className="text-textMuted">Drift</span>
|
|
145
|
-
<span className={`font-semibold tracking-wider ${
|
|
146
|
-
{
|
|
181
|
+
<span className={`font-semibold tracking-wider ${driftColor}`}>
|
|
182
|
+
{drift < 0.001 ? '0.000' : drift.toFixed(3)}s
|
|
183
|
+
</span>
|
|
184
|
+
</div>
|
|
185
|
+
<div className="flex justify-between items-center p-3 rounded-lg bg-black/30 border border-pulseBorder">
|
|
186
|
+
<span className="text-textMuted">Relay RTT</span>
|
|
187
|
+
<span className="text-textMain font-semibold tracking-wider">
|
|
188
|
+
{metrics ? `${Math.round(metrics.rtt)}ms` : '—'}
|
|
189
|
+
</span>
|
|
190
|
+
</div>
|
|
191
|
+
<div className="flex justify-between items-center p-3 rounded-lg bg-black/30 border border-pulseBorder">
|
|
192
|
+
<span className="text-textMuted">Msg Delay</span>
|
|
193
|
+
<span className={`font-semibold tracking-wider ${lastDelay !== null && lastDelay > 50 ? 'text-brandAmber' : 'text-brandEmerald'}`}>
|
|
194
|
+
{lastDelay !== null ? `${lastDelay}ms` : '—'}
|
|
195
|
+
{avgDelay !== null && <span className="text-textDim text-[10px] ml-1">avg {avgDelay}ms</span>}
|
|
147
196
|
</span>
|
|
148
197
|
</div>
|
|
149
198
|
</div>
|
|
150
199
|
</div>
|
|
151
200
|
|
|
201
|
+
{/* Pulse Event Log */}
|
|
152
202
|
<div className="glass-panel p-6 space-y-4">
|
|
153
203
|
<div className="flex items-center gap-2 text-xs font-bold uppercase tracking-widest text-textDim border-b border-pulseBorder pb-4">
|
|
154
204
|
<Users className="w-4 h-4 text-brandCyan" />
|
|
@@ -157,8 +207,8 @@ export default function App() {
|
|
|
157
207
|
|
|
158
208
|
<div className="flex flex-col gap-2 font-mono text-[11px] h-[300px] overflow-y-auto pr-2">
|
|
159
209
|
{events.length === 0 ? (
|
|
160
|
-
<div className="text-textMuted italic py-4 text-center">Waiting for events
|
|
161
|
-
) : events.map(ev => (
|
|
210
|
+
<div className="text-textMuted italic py-4 text-center">Waiting for events…</div>
|
|
211
|
+
) : events.map((ev: { id: number, action: string, time: string }) => (
|
|
162
212
|
<div key={ev.id} className="flex gap-3 items-center p-2.5 rounded-lg bg-black/30 border border-pulseBorder">
|
|
163
213
|
<span className="text-textDim shrink-0">{ev.time}</span>
|
|
164
214
|
<span className={`px-2 py-0.5 rounded font-bold uppercase tracking-wider ${ev.action.includes('SEND') ? 'bg-purple-500/20 text-purple-400' : 'bg-cyan-500/20 text-cyan-400'}`}>
|