@sansavision/create-pulse 0.4.4 → 0.4.6
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/dist/index.js +2 -0
- package/package.json +2 -2
- package/templates/aurora-auth-node-demo/README.md +43 -0
- package/templates/aurora-auth-node-demo/aurora.config.ts +15 -0
- package/templates/aurora-auth-node-demo/bun.lock +679 -0
- package/templates/aurora-auth-node-demo/drizzle.config.ts +9 -0
- package/templates/aurora-auth-node-demo/package.json +39 -0
- package/templates/aurora-auth-node-demo/postcss.config.mjs +7 -0
- package/templates/aurora-auth-node-demo/server.mjs +46 -0
- package/templates/aurora-auth-node-demo/src/actions/createMessage.action.server.ts +31 -0
- package/templates/aurora-auth-node-demo/src/aurora.auth.ts +65 -0
- package/templates/aurora-auth-node-demo/src/lib/auth-client.ts +30 -0
- package/templates/aurora-auth-node-demo/src/lib/auth.server.ts +11 -0
- package/templates/aurora-auth-node-demo/src/lib/auth.ts +30 -0
- package/templates/aurora-auth-node-demo/src/lib/db.ts +6 -0
- package/templates/aurora-auth-node-demo/src/lib/pulse.ts +45 -0
- package/templates/aurora-auth-node-demo/src/lib/schema.ts +107 -0
- package/templates/aurora-auth-node-demo/src/queries/listMessages.server.ts +25 -0
- package/templates/aurora-auth-node-demo/src/routes/api/auth/[...slug]/handler.ts +14 -0
- package/templates/aurora-auth-node-demo/src/routes/api/pulse/verify/handler.ts +55 -0
- package/templates/aurora-auth-node-demo/src/routes/auth/sign-in/page.client.tsx +132 -0
- package/templates/aurora-auth-node-demo/src/routes/auth/sign-in/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/auth/sign-up/page.client.tsx +154 -0
- package/templates/aurora-auth-node-demo/src/routes/auth/sign-up/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/arena-game/page.client.tsx +640 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/arena-game/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/chat/page.client.tsx +349 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/chat/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/encrypted-chat/page.client.tsx +472 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/encrypted-chat/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/game-sync/page.client.tsx +375 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/game-sync/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/queues/page.client.tsx +423 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/queues/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/video-call/page.client.tsx +840 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/video-call/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/watch-together/page.client.tsx +722 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/watch-together/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/layout.client.tsx +113 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/layout.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/page.client.tsx +195 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/favicon.ico +0 -0
- package/templates/aurora-auth-node-demo/src/routes/layout.tsx +18 -0
- package/templates/aurora-auth-node-demo/src/routes/page.client.tsx +263 -0
- package/templates/aurora-auth-node-demo/src/routes/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/styles/app.css +96 -0
- package/templates/aurora-auth-node-demo/tsconfig.json +27 -0
- package/templates/aurora-auth-node-demo/tsconfig.tsbuildinfo +1 -0
- package/templates/nextjs-auth-demo/next-env.d.ts +1 -1
- package/templates/nextjs-auth-demo/package.json +8 -7
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/arena-game/page.tsx +20 -3
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/chat/page.tsx +108 -23
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/encrypted-chat/page.tsx +278 -217
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/game-sync/page.tsx +66 -35
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/queues/page.tsx +213 -87
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/video-call/page.tsx +106 -6
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/watch-together/page.tsx +415 -262
- package/templates/nextjs-auth-node-demo/.env.example +10 -0
- package/templates/nextjs-auth-node-demo/Dockerfile +19 -0
- package/templates/nextjs-auth-node-demo/README.md +159 -0
- package/templates/nextjs-auth-node-demo/_gitignore +33 -0
- package/templates/nextjs-auth-node-demo/drizzle.config.ts +10 -0
- package/templates/nextjs-auth-node-demo/eslint.config.mjs +18 -0
- package/templates/nextjs-auth-node-demo/next-env.d.ts +6 -0
- package/templates/nextjs-auth-node-demo/next.config.ts +7 -0
- package/templates/nextjs-auth-node-demo/package.json +38 -0
- package/templates/nextjs-auth-node-demo/postcss.config.mjs +7 -0
- package/templates/nextjs-auth-node-demo/public/file.svg +1 -0
- package/templates/nextjs-auth-node-demo/public/globe.svg +1 -0
- package/templates/nextjs-auth-node-demo/public/next.svg +1 -0
- package/templates/nextjs-auth-node-demo/public/vercel.svg +1 -0
- package/templates/nextjs-auth-node-demo/public/window.svg +1 -0
- package/templates/nextjs-auth-node-demo/server.mjs +45 -0
- package/templates/nextjs-auth-node-demo/src/app/api/auth/[...all]/route.ts +4 -0
- package/templates/nextjs-auth-node-demo/src/app/api/pulse/verify/route.ts +54 -0
- package/templates/nextjs-auth-node-demo/src/app/auth/sign-in/page.tsx +131 -0
- package/templates/nextjs-auth-node-demo/src/app/auth/sign-up/page.tsx +153 -0
- package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/arena-game/page.tsx +640 -0
- package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/chat/page.tsx +349 -0
- package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/encrypted-chat/page.tsx +472 -0
- package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/game-sync/page.tsx +375 -0
- package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/queues/page.tsx +423 -0
- package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/video-call/page.tsx +840 -0
- package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/watch-together/page.tsx +724 -0
- package/templates/nextjs-auth-node-demo/src/app/dashboard/layout.tsx +113 -0
- package/templates/nextjs-auth-node-demo/src/app/dashboard/page.tsx +195 -0
- package/templates/nextjs-auth-node-demo/src/app/favicon.ico +0 -0
- package/templates/nextjs-auth-node-demo/src/app/globals.css +96 -0
- package/templates/nextjs-auth-node-demo/src/app/layout.tsx +27 -0
- package/templates/nextjs-auth-node-demo/src/app/page.tsx +254 -0
- package/templates/nextjs-auth-node-demo/src/lib/auth-client.ts +15 -0
- package/templates/nextjs-auth-node-demo/src/lib/auth.ts +14 -0
- package/templates/nextjs-auth-node-demo/src/lib/db.ts +6 -0
- package/templates/nextjs-auth-node-demo/src/lib/pulse.ts +45 -0
- package/templates/nextjs-auth-node-demo/src/lib/schema.ts +107 -0
- package/templates/nextjs-auth-node-demo/tsconfig.json +34 -0
|
@@ -27,6 +27,7 @@ interface Participant {
|
|
|
27
27
|
id: string;
|
|
28
28
|
name: string;
|
|
29
29
|
stream: MediaStream | null;
|
|
30
|
+
pc?: RTCPeerConnection;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
export default function VideoCallPage() {
|
|
@@ -35,6 +36,7 @@ export default function VideoCallPage() {
|
|
|
35
36
|
const [inCall, setInCall] = useState(false);
|
|
36
37
|
const [roomId, setRoomId] = useState("pulse-room-" + Math.random().toString(36).slice(2, 8));
|
|
37
38
|
const [joinRoomId, setJoinRoomId] = useState("");
|
|
39
|
+
const [joinError, setJoinError] = useState("");
|
|
38
40
|
const [localStream, setLocalStream] = useState<MediaStream | null>(null);
|
|
39
41
|
const [videoEnabled, setVideoEnabled] = useState(true);
|
|
40
42
|
const [audioEnabled, setAudioEnabled] = useState(true);
|
|
@@ -143,12 +145,12 @@ export default function VideoCallPage() {
|
|
|
143
145
|
const existing = prev.find((p) => p.id === remoteId);
|
|
144
146
|
if (existing) {
|
|
145
147
|
return prev.map((p) =>
|
|
146
|
-
p.id === remoteId ? { ...p, stream: remoteStream } : p
|
|
148
|
+
p.id === remoteId ? { ...p, stream: remoteStream, pc } : p
|
|
147
149
|
);
|
|
148
150
|
}
|
|
149
151
|
return [
|
|
150
152
|
...prev,
|
|
151
|
-
{ id: remoteId, name: remoteName, stream: remoteStream },
|
|
153
|
+
{ id: remoteId, name: remoteName, stream: remoteStream, pc },
|
|
152
154
|
];
|
|
153
155
|
});
|
|
154
156
|
};
|
|
@@ -189,6 +191,12 @@ export default function VideoCallPage() {
|
|
|
189
191
|
const joinCall = useCallback(
|
|
190
192
|
async (room: string) => {
|
|
191
193
|
if (!connRef.current || !session) return;
|
|
194
|
+
const validRoomRegex = /^pulse-room-[a-z0-9]{6}$/;
|
|
195
|
+
if (!validRoomRegex.test(room)) {
|
|
196
|
+
setJoinError("Invalid room ID format. Expected pulse-room-xxxxxx");
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
setJoinError("");
|
|
192
200
|
|
|
193
201
|
const stream = await getLocalMedia();
|
|
194
202
|
if (!stream) return;
|
|
@@ -423,6 +431,7 @@ export default function VideoCallPage() {
|
|
|
423
431
|
useEffect(() => {
|
|
424
432
|
if (localVideoRef.current && localStream && inCall) {
|
|
425
433
|
localVideoRef.current.srcObject = localStream;
|
|
434
|
+
localVideoRef.current.play().catch(() => { });
|
|
426
435
|
}
|
|
427
436
|
}, [localStream, inCall]);
|
|
428
437
|
|
|
@@ -544,8 +553,11 @@ export default function VideoCallPage() {
|
|
|
544
553
|
<input
|
|
545
554
|
type="text"
|
|
546
555
|
value={joinRoomId}
|
|
547
|
-
onChange={(e) =>
|
|
548
|
-
|
|
556
|
+
onChange={(e) => {
|
|
557
|
+
setJoinRoomId(e.target.value);
|
|
558
|
+
setJoinError("");
|
|
559
|
+
}}
|
|
560
|
+
placeholder="Enter room ID (pulse-room-xxxxxx)..."
|
|
549
561
|
className="flex-1 px-4 py-2.5 rounded-xl bg-slate-800/50 border border-slate-700 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 outline-none text-sm transition-colors placeholder:text-slate-600"
|
|
550
562
|
/>
|
|
551
563
|
<button
|
|
@@ -556,6 +568,7 @@ export default function VideoCallPage() {
|
|
|
556
568
|
Join
|
|
557
569
|
</button>
|
|
558
570
|
</div>
|
|
571
|
+
{joinError && <p className="text-red-400 text-xs mt-2">{joinError}</p>}
|
|
559
572
|
</div>
|
|
560
573
|
|
|
561
574
|
{/* Info */}
|
|
@@ -576,7 +589,7 @@ export default function VideoCallPage() {
|
|
|
576
589
|
<div className="h-full p-4 flex flex-col">
|
|
577
590
|
<div className={`flex-1 grid ${gridCols} gap-3 auto-rows-fr`}>
|
|
578
591
|
{/* Local Video */}
|
|
579
|
-
<div className="relative rounded-2xl overflow-hidden bg-slate-900 border border-slate-800">
|
|
592
|
+
<div className="relative rounded-2xl overflow-hidden bg-slate-900 border border-slate-800 group">
|
|
580
593
|
<video
|
|
581
594
|
ref={localVideoRef}
|
|
582
595
|
autoPlay
|
|
@@ -607,6 +620,19 @@ export default function VideoCallPage() {
|
|
|
607
620
|
Sharing Screen
|
|
608
621
|
</div>
|
|
609
622
|
)}
|
|
623
|
+
<button
|
|
624
|
+
onClick={(e) => {
|
|
625
|
+
const target = e.currentTarget.parentElement;
|
|
626
|
+
if (!document.fullscreenElement) {
|
|
627
|
+
target?.requestFullscreen().catch(() => { });
|
|
628
|
+
} else {
|
|
629
|
+
document.exitFullscreen().catch(() => { });
|
|
630
|
+
}
|
|
631
|
+
}}
|
|
632
|
+
className="absolute top-3 right-3 p-1.5 rounded-lg bg-black/50 hover:bg-black/70 text-white backdrop-blur transition-colors opacity-0 group-hover:opacity-100"
|
|
633
|
+
>
|
|
634
|
+
<Maximize className="w-4 h-4" />
|
|
635
|
+
</button>
|
|
610
636
|
</div>
|
|
611
637
|
|
|
612
638
|
{/* Remote Participants */}
|
|
@@ -707,15 +733,76 @@ export default function VideoCallPage() {
|
|
|
707
733
|
/* Remote video component */
|
|
708
734
|
function RemoteVideo({ participant }: { participant: Participant }) {
|
|
709
735
|
const videoRef = useRef<HTMLVideoElement>(null);
|
|
736
|
+
const [stats, setStats] = useState({ rtt: 0, bitrate: 0, packetLoss: 0 });
|
|
710
737
|
|
|
711
738
|
useEffect(() => {
|
|
712
739
|
if (videoRef.current && participant.stream) {
|
|
713
740
|
videoRef.current.srcObject = participant.stream;
|
|
741
|
+
videoRef.current.play().catch(() => { });
|
|
714
742
|
}
|
|
715
743
|
}, [participant.stream]);
|
|
716
744
|
|
|
745
|
+
// Gather WebRTC stats with delta-based bitrate
|
|
746
|
+
useEffect(() => {
|
|
747
|
+
if (!participant.pc || !participant.stream) return;
|
|
748
|
+
|
|
749
|
+
let prevBytesReceived = 0;
|
|
750
|
+
let prevTimestamp = Date.now();
|
|
751
|
+
|
|
752
|
+
const interval = setInterval(async () => {
|
|
753
|
+
try {
|
|
754
|
+
const pc = participant.pc!;
|
|
755
|
+
const report = await pc.getStats();
|
|
756
|
+
let rtt = 0;
|
|
757
|
+
let lossPercent = 0;
|
|
758
|
+
let currentBytesReceived = 0;
|
|
759
|
+
let packetsLost = 0;
|
|
760
|
+
let packetsReceived = 0;
|
|
761
|
+
|
|
762
|
+
report.forEach((stat) => {
|
|
763
|
+
if (stat.type === "candidate-pair" && stat.state === "succeeded") {
|
|
764
|
+
if (stat.currentRoundTripTime !== undefined) rtt = Math.round(stat.currentRoundTripTime * 1000);
|
|
765
|
+
}
|
|
766
|
+
if (stat.type === "inbound-rtp" && stat.kind === "video") {
|
|
767
|
+
if (stat.bytesReceived !== undefined) currentBytesReceived = stat.bytesReceived;
|
|
768
|
+
if (stat.packetsLost !== undefined) packetsLost = stat.packetsLost;
|
|
769
|
+
if (stat.packetsReceived !== undefined) packetsReceived = stat.packetsReceived;
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
// Compute bitrate from byte delta
|
|
774
|
+
const now = Date.now();
|
|
775
|
+
const timeDeltaSecs = (now - prevTimestamp) / 1000;
|
|
776
|
+
let bitrateKbps = 0;
|
|
777
|
+
if (prevBytesReceived > 0 && timeDeltaSecs > 0) {
|
|
778
|
+
bitrateKbps = Math.round(((currentBytesReceived - prevBytesReceived) * 8) / timeDeltaSecs / 1000);
|
|
779
|
+
}
|
|
780
|
+
prevBytesReceived = currentBytesReceived;
|
|
781
|
+
prevTimestamp = now;
|
|
782
|
+
|
|
783
|
+
// Compute loss percentage
|
|
784
|
+
const totalPackets = packetsLost + packetsReceived;
|
|
785
|
+
if (totalPackets > 0) {
|
|
786
|
+
lossPercent = Math.round((packetsLost / totalPackets) * 10000) / 100;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
setStats({ rtt, packetLoss: lossPercent, bitrate: bitrateKbps });
|
|
790
|
+
} catch { /* ignore */ }
|
|
791
|
+
}, 2000);
|
|
792
|
+
|
|
793
|
+
return () => clearInterval(interval);
|
|
794
|
+
}, [participant.pc, participant.stream]);
|
|
795
|
+
|
|
717
796
|
return (
|
|
718
|
-
<div className="relative rounded-2xl overflow-hidden bg-slate-900 border border-slate-800">
|
|
797
|
+
<div className="relative rounded-2xl overflow-hidden bg-slate-900 border border-slate-800 group">
|
|
798
|
+
{/* Stats Overlay - always visible when stream is active */}
|
|
799
|
+
{participant.stream && (
|
|
800
|
+
<div className="absolute top-3 left-3 flex flex-col gap-1 z-10 bg-black/60 p-2 rounded-lg backdrop-blur text-[10px] text-green-400 font-mono">
|
|
801
|
+
<div>RTT: {stats.rtt}ms</div>
|
|
802
|
+
<div>Loss: {stats.packetLoss}%</div>
|
|
803
|
+
<div>Bitrate: {stats.bitrate > 0 ? `${stats.bitrate} kbps` : "—"}</div>
|
|
804
|
+
</div>
|
|
805
|
+
)}
|
|
719
806
|
{participant.stream ? (
|
|
720
807
|
<video
|
|
721
808
|
ref={videoRef}
|
|
@@ -735,6 +822,19 @@ function RemoteVideo({ participant }: { participant: Participant }) {
|
|
|
735
822
|
{participant.name}
|
|
736
823
|
</span>
|
|
737
824
|
</div>
|
|
825
|
+
<button
|
|
826
|
+
onClick={(e) => {
|
|
827
|
+
const target = e.currentTarget.parentElement;
|
|
828
|
+
if (!document.fullscreenElement) {
|
|
829
|
+
target?.requestFullscreen().catch(() => { });
|
|
830
|
+
} else {
|
|
831
|
+
document.exitFullscreen().catch(() => { });
|
|
832
|
+
}
|
|
833
|
+
}}
|
|
834
|
+
className="absolute top-3 right-3 p-1.5 rounded-lg bg-black/50 hover:bg-black/70 text-white backdrop-blur transition-colors opacity-0 group-hover:opacity-100"
|
|
835
|
+
>
|
|
836
|
+
<Maximize className="w-4 h-4" />
|
|
837
|
+
</button>
|
|
738
838
|
</div>
|
|
739
839
|
);
|
|
740
840
|
}
|