Add Webapp

This commit is contained in:
clinton committed 2026-04-03 12:35:13 +02:00
commit ed924ef2ac
76 files changed
+17054

No files matched your search

@@ -0,0 +1,7 @@
import React from 'react'
export default function ErrorDialog({ error }: { error: string | Error }) {
return (
<div>{error instanceof Error ? error.message : error }</div>
)
}
+59
View File
@@ -0,0 +1,59 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import Link from "next/link"
export default function RoomCard({
title,
room,
image,
color,
}: {
title: string
room: string
image: any,
color: string
}) {
if(!image) return "No Image Available"
return <Link href={`/testb?room=${room}`}>
{image && <div
style={{
// minWidth: 500,
width: '100%',
height: 275,
overflow: 'hidden',
objectFit: 'cover',
objectPosition: 'center'
}}
className='relative opacity-70 hover:opacity-90'
>
<div className='text-2xl text-center w-full text-shadow-black text-shadow-2xs text-white bg-transparent absolute z-20 top-0'>{title}</div>
<img
width={'100%'}
height={275}
style={{
filter: 'grayscale(100%)',
position: 'relative',
overflow: 'hidden'
}}
src={image}
className='absolute top-0'
/>
<div
style={{
backgroundColor: color,
opacity: '50%',
// backgroundColor: 'rgba(100,27,198,0.5',
// width: 500,
width: '100%',
height: 275
}}
className='absolute top-0 bottom-0 left-0 right-0'
>
</div>
</div>
}
</Link>
}
+33
View File
@@ -0,0 +1,33 @@
import React from 'react'
import { Input } from '../ui/input'
import { InputGroup, InputGroupAddon, InputGroupButton } from '../ui/input-group'
import { Textarea } from '../ui/textarea'
export default function ChatBox() {
return (
<div className='flex flex-col'>
<div className='grow flex flex-col'>
CHAT MESSAGES
<div className='p-1 rounded-2xl border max-w-1/2 self-start'>Placeholder message</div>
<div className='p-1 rounded-2xl border max-w-1/2 self-end'>Placeholder message</div>
</div>
<div>
<div className="grid w-full max-w-sm gap-6">
<InputGroup>
<Textarea
data-slot="input-group-control"
className="flex field-sizing-content min-h-16 w-full resize-none rounded-md bg-transparent px-3 py-2.5 text-base transition-[color,box-shadow] outline-none md:text-sm"
placeholder="Message..."
/>
<InputGroupAddon align="block-end">
<InputGroupButton className="ml-auto" size="sm" variant="default">
Send
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</div>
</div>
</div>
)
}
@@ -0,0 +1,175 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import React, { useRef } from 'react'
export default function ChatNoServerWidget() {
const localVideo = useRef<HTMLVideoElement | null>(null)
const remoteVideo = useRef<HTMLVideoElement | null>(null)
const localStream = useRef<MediaStream>(null)
const pc1 = useRef<RTCPeerConnection>(null)
const pc2 = useRef<RTCPeerConnection>(null)
const handleStart = async () => {
console.log('Requesting local stream');
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
console.log('Received local stream');
localVideo.current!.srcObject = stream;
localStream.current = stream;
} catch (e) {
console.error(e)
alert(`getUserMedia() error: ${(e as Error).name}`);
}
}
function getName(pc: RTCPeerConnection) {
return (pc === pc1.current) ? 'pc1' : 'pc2';
}
function getOtherPc(pc: RTCPeerConnection) {
return (pc === pc1.current) ? pc2.current : pc1.current;
}
async function onIceCandidate(pc: RTCPeerConnection, event: RTCPeerConnectionIceEvent) {
try {
await (getOtherPc(pc)!.addIceCandidate(event.candidate));
onAddIceCandidateSuccess(pc);
} catch (e) {
onAddIceCandidateError(pc, e);
}
console.log(`${getName(pc)} ICE candidate:\n${event.candidate ? event.candidate.candidate : '(null)'}`);
}
function onAddIceCandidateSuccess(pc: RTCPeerConnection) {
console.log(`${getName(pc)} addIceCandidate success`);
}
function onAddIceCandidateError(pc: RTCPeerConnection, error: any) {
console.log(`${getName(pc)} failed to add ICE Candidate: ${error.toString()}`);
}
function onIceStateChange(pc: RTCPeerConnection, event: Event) {
if (pc) {
console.log(`${getName(pc)} ICE state: ${pc.iceConnectionState}`);
console.log('ICE state change event: ', event);
}
}
function gotRemoteStream(e: RTCTrackEvent) {
if (remoteVideo.current!.srcObject !== e.streams[0]) {
remoteVideo.current!.srcObject = e.streams[0];
console.log('pc2 received remote stream');
}
}
async function call() {
console.log('Starting call');
const videoTracks = localStream.current!.getVideoTracks();
const audioTracks = localStream.current!.getAudioTracks();
if (videoTracks.length > 0) {
console.log(`Using video device: ${videoTracks[0].label}`);
}
if (audioTracks.length > 0) {
console.log(`Using audio device: ${audioTracks[0].label}`);
}
const configuration = {};
console.log('RTCPeerConnection configuration:', configuration);
pc1.current = new RTCPeerConnection(configuration);
console.log('Created local peer connection object pc1');
pc1.current.addEventListener('icecandidate', e => onIceCandidate(pc1.current!, e));
pc2.current = new RTCPeerConnection(configuration);
console.log('Created remote peer connection object pc2');
pc2.current.addEventListener('icecandidate', e => onIceCandidate(pc2.current!, e));
pc1.current.addEventListener('iceconnectionstatechange', e => onIceStateChange(pc1.current!, e));
pc2.current.addEventListener('iceconnectionstatechange', e => onIceStateChange(pc2.current!, e));
pc2.current.addEventListener('track', gotRemoteStream);
localStream.current!.getTracks().forEach(track => pc1.current!.addTrack(track, localStream.current!));
console.log('Added local stream to pc1');
try {
console.log('pc1 createOffer start');
const offer = await pc1.current.createOffer();
// const offer = await pc1.current.createOffer(offerOptions);
await onCreateOfferSuccess(offer);
} catch (e) {
onCreateSessionDescriptionError(e);
}
}
function onSetLocalSuccess(pc: RTCPeerConnection) {
console.log(`${getName(pc)} setLocalDescription complete`);
}
function onSetRemoteSuccess(pc: RTCPeerConnection) {
console.log(`${getName(pc)} setRemoteDescription complete`);
}
function onSetSessionDescriptionError(error: any) {
console.log(`Failed to set session description: ${error.toString()}`);
}
async function onCreateOfferSuccess(desc: RTCSessionDescriptionInit) {
console.log(`Offer from pc1\n${desc.sdp}`);
console.log('pc1 setLocalDescription start');
try {
await pc1.current!.setLocalDescription(desc);
onSetLocalSuccess(pc1.current!);
} catch (e) {
onSetSessionDescriptionError(e);
}
console.log('pc2 setRemoteDescription start');
try {
await pc2.current!.setRemoteDescription(desc);
onSetRemoteSuccess(pc2.current!);
} catch (e) {
onSetSessionDescriptionError(e);
}
console.log('pc2 createAnswer start');
// Since the 'remote' side has no media stream we need
// to pass in the right constraints in order for it to
// accept the incoming offer of audio and video.
try {
const answer = await pc2.current!.createAnswer();
await onCreateAnswerSuccess(answer);
} catch (e) {
onCreateSessionDescriptionError(e);
}
}
async function onCreateAnswerSuccess(desc: RTCSessionDescriptionInit) {
console.log(`Answer from pc2:\n${desc.sdp}`);
console.log('pc2 setLocalDescription start');
try {
await pc2.current!.setLocalDescription(desc);
onSetLocalSuccess(pc2.current!);
} catch (e) {
onSetSessionDescriptionError(e);
}
console.log('pc1 setRemoteDescription start');
try {
await pc1.current!.setRemoteDescription(desc);
onSetRemoteSuccess(pc1.current!);
} catch (e) {
onSetSessionDescriptionError(e);
}
}
function onCreateSessionDescriptionError(error: any) {
console.log(`Failed to create session description: ${error.toString()}`);
}
return (
<div>
<video ref={localVideo} id="localVideo" playsInline autoPlay muted></video>
<video ref={remoteVideo} id="remoteVideo" playsInline autoPlay></video>
<div className="box">
<button onClick={handleStart} id="startButton">Start</button>
<button onClick={() => call()} id="callButton">Call</button>
<button id="hangupButton">Hang Up</button>
</div>
</div>
)
}
@@ -0,0 +1,175 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import React, { useRef } from 'react'
export default function ChatNoServerWidget() {
const localVideo = useRef<HTMLVideoElement | null>(null)
const remoteVideo = useRef<HTMLVideoElement | null>(null)
const localStream = useRef<MediaStream>(null)
const pc1 = useRef<RTCPeerConnection>(null)
const pc2 = useRef<RTCPeerConnection>(null)
const handleStart = async () => {
console.log('Requesting local stream');
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
console.log('Received local stream');
localVideo.current!.srcObject = stream;
localStream.current = stream;
} catch (e) {
console.error(e)
alert(`getUserMedia() error: ${(e as Error).name}`);
}
}
function getName(pc: RTCPeerConnection) {
return (pc === pc1.current) ? 'pc1' : 'pc2';
}
function getOtherPc(pc: RTCPeerConnection) {
return (pc === pc1.current) ? pc2.current : pc1.current;
}
async function onIceCandidate(pc: RTCPeerConnection, event: RTCPeerConnectionIceEvent) {
try {
await (getOtherPc(pc)!.addIceCandidate(event.candidate));
onAddIceCandidateSuccess(pc);
} catch (e) {
onAddIceCandidateError(pc, e);
}
console.log(`${getName(pc)} ICE candidate:\n${event.candidate ? event.candidate.candidate : '(null)'}`);
}
function onAddIceCandidateSuccess(pc: RTCPeerConnection) {
console.log(`${getName(pc)} addIceCandidate success`);
}
function onAddIceCandidateError(pc: RTCPeerConnection, error: any) {
console.log(`${getName(pc)} failed to add ICE Candidate: ${error.toString()}`);
}
function onIceStateChange(pc: RTCPeerConnection, event: Event) {
if (pc) {
console.log(`${getName(pc)} ICE state: ${pc.iceConnectionState}`);
console.log('ICE state change event: ', event);
}
}
function gotRemoteStream(e: RTCTrackEvent) {
if (remoteVideo.current!.srcObject !== e.streams[0]) {
remoteVideo.current!.srcObject = e.streams[0];
console.log('pc2 received remote stream');
}
}
async function call() {
console.log('Starting call');
const videoTracks = localStream.current!.getVideoTracks();
const audioTracks = localStream.current!.getAudioTracks();
if (videoTracks.length > 0) {
console.log(`Using video device: ${videoTracks[0].label}`);
}
if (audioTracks.length > 0) {
console.log(`Using audio device: ${audioTracks[0].label}`);
}
const configuration = {};
console.log('RTCPeerConnection configuration:', configuration);
pc1.current = new RTCPeerConnection(configuration);
console.log('Created local peer connection object pc1');
pc1.current.addEventListener('icecandidate', e => onIceCandidate(pc1.current!, e));
pc2.current = new RTCPeerConnection(configuration);
console.log('Created remote peer connection object pc2');
pc2.current.addEventListener('icecandidate', e => onIceCandidate(pc2.current!, e));
pc1.current.addEventListener('iceconnectionstatechange', e => onIceStateChange(pc1.current!, e));
pc2.current.addEventListener('iceconnectionstatechange', e => onIceStateChange(pc2.current!, e));
pc2.current.addEventListener('track', gotRemoteStream);
localStream.current!.getTracks().forEach(track => pc1.current!.addTrack(track, localStream.current!));
console.log('Added local stream to pc1');
try {
console.log('pc1 createOffer start');
const offer = await pc1.current.createOffer();
// const offer = await pc1.current.createOffer(offerOptions);
await onCreateOfferSuccess(offer);
} catch (e) {
onCreateSessionDescriptionError(e);
}
}
function onSetLocalSuccess(pc: RTCPeerConnection) {
console.log(`${getName(pc)} setLocalDescription complete`);
}
function onSetRemoteSuccess(pc: RTCPeerConnection) {
console.log(`${getName(pc)} setRemoteDescription complete`);
}
function onSetSessionDescriptionError(error: any) {
console.log(`Failed to set session description: ${error.toString()}`);
}
async function onCreateOfferSuccess(desc: RTCSessionDescriptionInit) {
console.log(`Offer from pc1\n${desc.sdp}`);
console.log('pc1 setLocalDescription start');
try {
await pc1.current!.setLocalDescription(desc);
onSetLocalSuccess(pc1.current!);
} catch (e) {
onSetSessionDescriptionError(e);
}
console.log('pc2 setRemoteDescription start');
try {
await pc2.current!.setRemoteDescription(desc);
onSetRemoteSuccess(pc2.current!);
} catch (e) {
onSetSessionDescriptionError(e);
}
console.log('pc2 createAnswer start');
// Since the 'remote' side has no media stream we need
// to pass in the right constraints in order for it to
// accept the incoming offer of audio and video.
try {
const answer = await pc2.current!.createAnswer();
await onCreateAnswerSuccess(answer);
} catch (e) {
onCreateSessionDescriptionError(e);
}
}
async function onCreateAnswerSuccess(desc: RTCSessionDescriptionInit) {
console.log(`Answer from pc2:\n${desc.sdp}`);
console.log('pc2 setLocalDescription start');
try {
await pc2.current!.setLocalDescription(desc);
onSetLocalSuccess(pc2.current!);
} catch (e) {
onSetSessionDescriptionError(e);
}
console.log('pc1 setRemoteDescription start');
try {
await pc1.current!.setRemoteDescription(desc);
onSetRemoteSuccess(pc1.current!);
} catch (e) {
onSetSessionDescriptionError(e);
}
}
function onCreateSessionDescriptionError(error: any) {
console.log(`Failed to create session description: ${error.toString()}`);
}
return (
<div>
<video ref={localVideo} id="localVideo" playsInline autoPlay muted></video>
<video ref={remoteVideo} id="remoteVideo" playsInline autoPlay></video>
<div className="box">
<button onClick={handleStart} id="startButton">Start</button>
<button onClick={() => call()} id="callButton">Call</button>
<button id="hangupButton">Hang Up</button>
</div>
</div>
)
}
+134
View File
@@ -0,0 +1,134 @@
'use client'
/* eslint-disable @typescript-eslint/no-explicit-any */
import React, { useEffect, useRef, useState } from 'react'
import { Socket } from 'socket.io';
import { io } from 'socket.io-client';
import { socket } from './socket';
import Link from 'next/link';
// https://www.baeldung.com/webrtc
export default function ChatWS() {
const [session, setSession] = useState<{
who: string,
room?: string
}>()
const [messages, setMessages] = useState<{
who: string,
msg: string
}[]>([])
const nameRef = useRef<HTMLInputElement | null>(null)
useEffect(() => {
socket.onAny((ev, data) => {
console.log(`ANY`, ev, data)
// setMessages(prevMessages => [...prevMessages, data]);
});
socket.on('chat message', (data) => {
// console.log(`chat message`,data)
setMessages(prevMessages => [...prevMessages, data]);
});
// Clean up the listener when the component unmounts
return () => {
socket.off('chat message');
};
}, [])
if (!session?.who) return <div
style={{ 'background': 'conic-gradient(from 210deg, #c6bcb9 0.000deg, #c6bcb9 24.000deg, #b9b6b9 calc(24.000deg + 0.1deg), #b9b6b9 48.000deg, #aab0b8 calc(48.000deg + 0.1deg), #aab0b8 72.000deg, #9ba9b6 calc(72.000deg + 0.1deg), #9ba9b6 96.000deg, #8ca2b4 calc(96.000deg + 0.1deg), #8ca2b4 120.000deg, #7e99b0 calc(120.000deg + 0.1deg), #7e99b0 144.000deg, #7191ac calc(144.000deg + 0.1deg), #7191ac 168.000deg, #6688a7 calc(168.000deg + 0.1deg), #6688a7 192.000deg, #5d7fa2 calc(192.000deg + 0.1deg), #5d7fa2 216.000deg, #56769c calc(216.000deg + 0.1deg), #56769c 240.000deg, #526d95 calc(240.000deg + 0.1deg), #526d95 264.000deg, #50648e calc(264.000deg + 0.1deg), #50648e 288.000deg, #515b86 calc(288.000deg + 0.1deg), #515b86 312.000deg, #55547e calc(312.000deg + 0.1deg), #55547e 336.000deg, #5c4c75 calc(336.000deg + 0.1deg) 360.000deg)' }}
className='bg-gray-100 h-svh w-screen z-30 fixed flex items-center justify-center align-middle'>
<div
// onSubmit={ev => {
// ev.preventDefault()
// const form = new FormData(ev.target)
// alert(form.get(`name`))
// setSession((s: any) => {
// return {
// ...s,
// who: form.get(`name`)
// }
// })
// }}
className='bg-white/10 backdrop-blur-lg w-96 h-64 flex flex-col border border-white/20 shadow-lg p-2'>
<div className='grow font-black text-center'>
Enter a name to use in this chat
</div>
<div className='grow'>
<input
ref={nameRef}
className='w-full focus:p-2 transition-all focus:bg-white'
autoFocus
name='name'
placeholder='Your name' />
</div>
<button type='button'
onClick={() => {
setSession((s: any) => {
return {
...s,
who: nameRef.current?.value
}
})
}} className='shadow p-2'>Next</button>
</div>
</div>
return (
<div
/* https://grabient.com/HQNgrAHANMYIxhgJjAFigWlgTkVsaM2AzHJsMRAAzlKrTBwhJRwVgv4gjnYDsfcsWzpQSYq2BVpUKkA?style=angularSwatches&angle=210&steps=15 */
// background: conic-gradient(from 210deg, #7c6f6f 0.000deg, #7c6f6f 24.000deg, #6e6b6d calc(24.000deg + 0.1deg), #6e6b6d 48.000deg, #5f666a calc(48.000deg + 0.1deg), #5f666a 72.000deg, #505f67 calc(72.000deg + 0.1deg), #505f67 96.000deg, #3f5862 calc(96.000deg + 0.1deg), #3f5862 120.000deg, #2f515e calc(120.000deg + 0.1deg), #2f515e 144.000deg, #204858 calc(144.000deg + 0.1deg), #204858 168.000deg, #123f52 calc(168.000deg + 0.1deg), #123f52 192.000deg, #05364b calc(192.000deg + 0.1deg), #05364b 216.000deg, #002c43 calc(216.000deg + 0.1deg), #002c43 240.000deg, #00233b calc(240.000deg + 0.1deg), #00233b 264.000deg, #001933 calc(264.000deg + 0.1deg), #001933 288.000deg, #000f2a calc(288.000deg + 0.1deg), #000f2a 312.000deg, #000621 calc(312.000deg + 0.1deg), #000621 336.000deg, #000017 calc(336.000deg + 0.1deg) 360.000deg);
style={{ 'background': 'black' }}
// style={{ 'background': 'conic-gradient(from 210deg, #c6bcb9 0.000deg, #c6bcb9 24.000deg, #b9b6b9 calc(24.000deg + 0.1deg), #b9b6b9 48.000deg, #aab0b8 calc(48.000deg + 0.1deg), #aab0b8 72.000deg, #9ba9b6 calc(72.000deg + 0.1deg), #9ba9b6 96.000deg, #8ca2b4 calc(96.000deg + 0.1deg), #8ca2b4 120.000deg, #7e99b0 calc(120.000deg + 0.1deg), #7e99b0 144.000deg, #7191ac calc(144.000deg + 0.1deg), #7191ac 168.000deg, #6688a7 calc(168.000deg + 0.1deg), #6688a7 192.000deg, #5d7fa2 calc(192.000deg + 0.1deg), #5d7fa2 216.000deg, #56769c calc(216.000deg + 0.1deg), #56769c 240.000deg, #526d95 calc(240.000deg + 0.1deg), #526d95 264.000deg, #50648e calc(264.000deg + 0.1deg), #50648e 288.000deg, #515b86 calc(288.000deg + 0.1deg), #515b86 312.000deg, #55547e calc(312.000deg + 0.1deg), #55547e 336.000deg, #5c4c75 calc(336.000deg + 0.1deg) 360.000deg)' }}
// style={{ 'background': 'conic-gradient(from 210deg, #7c6f6f 0.000deg, #7c6f6f 24.000deg, #6e6b6d calc(24.000deg + 0.1deg), #6e6b6d 48.000deg, #5f666a calc(48.000deg + 0.1deg), #5f666a 72.000deg, #505f67 calc(72.000deg + 0.1deg), #505f67 96.000deg, #3f5862 calc(96.000deg + 0.1deg), #3f5862 120.000deg, #2f515e calc(120.000deg + 0.1deg), #2f515e 144.000deg, #204858 calc(144.000deg + 0.1deg), #204858 168.000deg, #123f52 calc(168.000deg + 0.1deg), #123f52 192.000deg, #05364b calc(192.000deg + 0.1deg), #05364b 216.000deg, #002c43 calc(216.000deg + 0.1deg), #002c43 240.000deg, #00233b calc(240.000deg + 0.1deg), #00233b 264.000deg, #001933 calc(264.000deg + 0.1deg), #001933 288.000deg, #000f2a calc(288.000deg + 0.1deg), #000f2a 312.000deg, #000621 calc(312.000deg + 0.1deg), #000621 336.000deg, #000017 calc(336.000deg + 0.1deg) 360.000deg)' }}
className='flex flex-col h-svh w-screen'>
<div className='bg-purple-900 shadow-sm shadow-purple-800'>
<Link href={`/`}>
<button className='bg-purple-950 text-white p-2 shadow hover:bg-purple-700'>Close Chat</button>
</Link>
</div>
<div className='grow flex flex-col overflow-auto gap-2'>
{messages.map((msg, i) =>
<div
className={`w-1/2 flex flex-col bg-gray-100 p-2 rounded-2xl shadow ${msg.who === session.who ? 'self-start' : 'self-end'}`}
key={`message-${i}`}>
<small>{msg.who}</small>
{msg.msg}
</div>
)}
</div>
{/* {JSON.stringify(messages)} */}
<div className='text-center text-amber-600 bg-amber-100'>
All message are removed once you leave this chat
</div>
<form onSubmit={ev => {
ev.preventDefault()
const form = new FormData(ev.target)
if (!form.get('msg')) return
socket.emit('chat message', {
who: session!.who ?? 'Annonymous',
msg: form.get('msg') as string
})
ev.currentTarget.reset()
}}
className='flex p-2 bg-gray-100 shadow-2xl opacity-90'
>
<input placeholder='Message' name='msg' className='grow' />
<button
className='shadow p-2 bg-gray-300'
onClick={() => {
// socket.emit('chat message', {
// who: 'Clint',
// msg: `Hello`
// })
// socket.send(`Hello MSG`)
}
}>Send</button>
</form>
</div>
)
}
+388
View File
@@ -0,0 +1,388 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import { useEffect, useRef, useState } from "react"
import { Device } from 'mediasoup-client'
import { Transport } from "mediasoup-client/types"
const wsURL = 'ws://localhost:4000'
let remoteStream
export default function MediaSoupWidget() {
const socket = useRef<WebSocket | undefined>(undefined)
const device = useRef<Device | undefined>(undefined)
const videoRef = useRef<HTMLVideoElement | null>(null)
const videoBRef = useRef<HTMLVideoElement | null>(null)
const consumerTransport = useRef<Transport | null>(null)
const [connected, setConnected] = useState<boolean>(false)
const [states, setStates] = useState<string[]>([])
const loadDevices = async (routerCapabilities: any) => {
try {
device.current = new Device()
await device.current!.load({ routerRtpCapabilities: routerCapabilities })
// console.log(`Supported : `, device)
setConnected(true)
} catch (e) {
if ((e as any).name === 'UnsupportedErro') {
console.log(`Not Supported`)
}
console.error(`Faield to create device:`, e)
}
}
const getUserMedia = async (transport: any, isWebCam: boolean) => {
if (!device.current?.canProduce('video')) {
console.error(`Cant stream video`)
return
}
let stream
try {
stream = isWebCam ? await navigator.mediaDevices.getUserMedia({
video: true, audio: true
}) : await navigator.mediaDevices.getDisplayMedia({ video: true })
} catch (e) {
console.error(`Unable to get device to stream`, e)
}
return stream
}
const onProducerTransportCreated = async (producerDetails: any) => {
// 5. Createe Send Transport
setStates(s => [...s, 'createSendTransport'])
const transport = device.current?.createSendTransport({ ...producerDetails })
transport?.on('connect', async ({ dtlsParameters }, callback, errback) => {
// 6. connectProducerTransport
setStates(s => [...s, 'createSendTransport'])
socket.current?.send(JSON.stringify({
type: 'connectProducerTransport',
dtlsParameters
}))
socket.current?.addEventListener('message', ev => {
// 7. producerTransportConnected
// console.log(`SEOM TO CHK `, ev.data)
if (JSON.parse(ev.data).type === 'producerTransportConnected') {
setStates(s => [...s, 'producerTransportConnected'])
// console.log(`------->producerTransportConnected:onCB`)
callback()
}
})
})
transport?.on('produce', async ({ kind, rtpParameters }, callback, errback) => {
// 8. Produce
setStates(s => [...s, 'produce'])
socket.current?.send(JSON.stringify({
type: 'produce',
transportId: transport.id,
kind,
rtpParameters
}))
socket.current?.addEventListener('message', ev => {
if (ev.data.type === 'published') {
callback(ev.data.id``)
}
})
})
transport?.on('connectionstatechange', async (state) => {
console.log(`connectionstatechange`, state)
switch (state) {
case 'connecting': break;
case 'closed': break;
case 'connected':
// Link stream here
break;
case 'disconnected': break;
case 'failed':
transport.close()
break;
case 'new': break;
}
})
transport?.on('icecandidateerror', async (error) => {
console.log('icecandidateerror', error)
})
transport?.on('icegatheringstatechange', async (error) => {
console.log('icegatheringstatechange', error)
})
transport?.on('producedata', async (error) => {
console.log('producedata', error)
})
let streamMedia
try {
streamMedia = await getUserMedia(transport, true) // is webcam
const track = streamMedia!.getVideoTracks()[0]
// const params - { track }
// videoRef.current?.h = track
console.log(`LocalStream`, streamMedia)
// videoRef.current!.srcObject = streamMedia
const producer = await transport?.produce({ track })
producer!.on("trackended", () => {
console.log("track ended");
});
producer!.on("transportclose", () => {
console.log("transport ended");
});
}
catch (e) {
console.log(`ERROR`, e)
}
}
const onSubTransportCreated = (consumerDetails: any) => {
consumerTransport.current = device.current!.createRecvTransport({ ...consumerDetails })
console.log(`onSubTransportCreated`, consumerDetails, consumerTransport.current.connectionState)
consumerTransport.current.on('connect', ({ dtlsParameters }, callback, errback) => {
//11 . Accept connect
setStates(s => [...s, 'connectConsumerTransport'])
socket.current?.send(JSON.stringify({
type: 'connectConsumerTransport',
transportId: consumerTransport.current!.id,
dtlsParameters
}))
socket.current?.addEventListener('message', ev => {
if (JSON.parse(ev.data).type === 'subConnected') {
console.log(`subConnected**`, JSON.parse(ev.data).type)
callback()
}
})
})
consumerTransport.current?.on('connectionstatechange', async (state) => {
console.log(`connectionstatechange:`, state)
switch (state) {
case 'connecting':
break;
case 'failed': console.log(`FILAED`); break;
case 'connected':
console.log(`remoteStream`, remoteStream)
// videoBRef.current!.srcObject = remoteStream
socket.current?.send(JSON.stringify({
type: 'resume',
}))
break;
default:
break;
}
})
consumerTransport.current?.on('icecandidateerror', () => {
console.log(`icecandidateerror`)
})
consumerTransport.current?.on('icegatheringstatechange', async (state) => {
console.log(`icegatheringstatechange`, state)
})
const stream = consumer(consumerTransport.current)
}
const onSubscribe = async (details: any) => {
console.log('onSubscribe', details)
const {
producerId,
id,
kind,
rtpParameters,
type,
producerPaused,
} = details
const codecOptions = {}
const consumer = await consumerTransport.current!.consume({
producerId,
id,
kind,
rtpParameters,
// type,
// producerPaused,
// codecOptions
})
const stream = new MediaStream()
stream.addTrack(consumer.track)
console.log(`TRYIN STREAM 1`,stream)
videoBRef.current!.srcObject = stream
}
const consumer = async (transport: any) => {
const rtpCapabilities = device.current?.recvRtpCapabilities
socket.current?.send(JSON.stringify({
type: 'consume',
rtpCapabilities
}))
}
// const connectSendTransport = async () => {
// const producer = await transport.produce(params);
// console.log("Producer created:", producer.id, producer.kind);
// producer.on("trackended", () => {
// console.log("track ended");
// });
// producer.on("transportclose", () => {
// console.log("transport ended");
// });
// };
const stream = () => {
// 4.1 Start creating producers steam
// 4. Producers stream
setStates(s => [...s, 'createProducerTransport'])
socket.current?.send(JSON.stringify({
type: 'createProducerTransport',
forceTcp: false,
rtpCapabilities: device.current?.sendRtpCapabilities
}))
}
const joinStream = () => {
setStates(s => [...s, 'createConsumerTransport'])
socket.current?.send(JSON.stringify({
type: 'createConsumerTransport',
forceTcp: false,
// rtpCapabilities: device.current?.sendRtpCapabilities
}))
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const parseWSMessage = (ev: any) => {
const recv = JSON.parse(ev)
// console.log(`-- parseWSMessage --`, recv.data)
switch (recv.type) {
case 'routerCapabilities':
// 3. Received capabilities
setStates(s => [...s, 'routerCapabilities'])
loadDevices(recv.data);
break;
case 'producerTransportCreated':
// 4.2 Received capabilities
setStates(s => [...s, 'producerTransportCreated'])
onProducerTransportCreated(recv.data);
break;
// case 'producerTransportConnected':
// // 7. producerTransportConnected but not with callback
// setStates(s => [...s, 'producerTransportCreated'])
// // callback()
// break;
case 'newProducer':
// 9 Found new Produce contents and send to all clients
setStates(s => [...s, 'newProducer'])
break;
case 'subTransportCreated':
// 10 Consumer joined
setStates(s => [...s, 'subTransportCreated'])
onSubTransportCreated(recv.data);
break;
case 'resumed':
// 10 Consumer joined
setStates(s => [...s, 'resumed'])
console.log(`resumed`, recv.data)
break;
case 'subscribed':
// 12 Consumer joined
setStates(s => [...s, 'subscribed'])
onSubscribe(recv.data)
break;
default:
console.log(`Received Uknown`, recv.type)
break;
}
// console.log(ev)
}
return (
<div>WebRTCChat
<div>
<div className={`${states.includes('ws-connected') ? 'text-green-400' : 'text-gray-400'} border`}>
<div>Connected To Server</div>
<small>Establish connection to the WS Server</small>
</div>
<div className={`${states.includes('getRouterRtpCapabilities') ? 'text-green-400' : 'text-gray-400'} border`}>
<div>Checking Server Capabilities</div>
<small>getRouterRtpCapabilities</small>
</div>
<div className={`${states.includes('routerCapabilities') ? 'text-green-400' : 'text-gray-400'} border`}>
<div>Has Server Capabilities</div>
<small>routerCapabilities</small>
</div>
<div className={`${states.includes('createProducerTransport') ? 'text-green-400' : 'text-gray-400'} border`}>
<div>Create Producers Transport (Waiting for user to start stream)</div>
<small>createProducerTransport</small>
</div>
<div className={`${states.includes('producerTransportCreated') ? 'text-green-400' : 'text-gray-400'} border`}>
<div>Created Producers Transport</div>
<small>producerTransportCreated</small>
</div>
<div className={`${states.includes('producerTransportCreated') ? 'text-green-400' : 'text-gray-400'} border`}>
<div>Created Producers Transport</div>
<small>producerTransportCreated</small>
</div>
<div className={`${states.includes('createSendTransport') ? 'text-green-400' : 'text-gray-400'} border`}>
<div>Created Send Transport</div>
<small>createSendTransport</small>
</div>
<div className={`${states.includes('producerTransportConnected') ? 'text-green-400' : 'text-gray-400'} border`}>
<div>Producer Transport Connected</div>
<small>producerTransportConnected</small>
</div>
<div className={`${states.includes('producerTransportConnected') ? 'text-green-400' : 'text-gray-400'} border`}>
<div>Send Produced</div>
<small>produce</small>
</div>
<div className={`${states.includes('newProducer') ? 'text-green-400' : 'text-gray-400'} border`}>
<div>Found new Produce contents and send to all clients</div>
<small>newProducer</small>
</div>
<div className={`${states.includes('subTransportCreated') ? 'text-green-400' : 'text-gray-400'} border`}>
<div>Cunsumber connected</div>
<small>subTransportCreated</small>
</div>
<div className={`${states.includes('connectConsumerTransport') ? 'text-green-400' : 'text-gray-400'} border`}>
<div>Accept Consumer Connection</div>
<small>connectConsumerTransport</small>
</div>
</div>
<button
onClick={() => {
socket.current = new WebSocket(wsURL)
socket.current.onopen = () => {
// 1. Create websocket connection
setStates(s => [...s, 'ws-connected'])
const msg = {
type: "getRouterRtpCapabilities"
}
try {
// 2. Get Server capabilities
setStates(s => [...s, 'getRouterRtpCapabilities'])
socket.current?.send(JSON.stringify(msg))
} catch (e) {
console.log(`Failed to send capabilities`, e)
}
}
socket.current.onmessage = e => parseWSMessage((e as any).data)
}}
disabled={connected}
>{connected ? 'Connected' : 'Connect'}</button>
<button onClick={() => stream()}>
Stream
</button>
<video ref={videoRef}></video>
<button
onClick={() => {
joinStream()
}}
>Join</button>
<video ref={videoBRef}></video>
</div>
)
}
@@ -0,0 +1,187 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import { CameraIcon, HdIcon, MicrochipIcon, Server, VideoIcon } from 'lucide-react';
import Peer, { DataConnection } from 'peerjs';
import React, { ReactNode, useCallback, useEffect, useRef, useState } from 'react'
type FlowStateType = {
color: 'red' | 'blue' | 'green' | 'orange',
flow: 'MIC' | 'VIDEO' | 'SERVER' | 'NO_PEERS' | 'ROOM',
loading: boolean
}
export default function WebRTCChatUsingPeer() {
let mediaStream
// UI
const peerRef = useRef<Peer>(undefined)
const [peers, setPeers] = useState<string[]>([])
const connection = useRef<DataConnection>(undefined)
const videoRef = useRef<HTMLVideoElement>(null)
// const [mediaStream, setMediaStream] = useState<MediaStream>(null)
const [received, setReceivved] = useState<string>('')
const [peerId, setPeerId] = useState<string>('')
const [theirPeerId, setTheirPeerId] = useState<string>('')
const [myStream, setMyStream] = useState<MediaStream>(null);
const [devices, setDevices] = useState<MediaDeviceInfo[]>([])
const [selectedDevice, setSelectedDevice] = useState<{ video: { deviceId: { exact: string } }, audio: { deviceId: { exact: string } } }>()
const peer = new Peer({
host: "localhost",
port: 4000,
path: "/myapp",
secure: false, // Use true with HTTPS
});
useEffect(() => {
peer.on("open", function (id) {
setPeerId(id)
console.log("My peer ID is: " + id);
});
}, [])
peer.on('connection', function (conn) {
console.log(`Got Connection`, conn)
conn.on("data", function (data) {
setReceivved(data as string)
console.log("Received", data);
});
conn.send("Hello!");
});
// useEffect(() => {
// // Get user media
// navigator.mediaDevices.getUserMedia({ video: true, audio: true })
// .then(stream => {
// // setMyStream(stream);
// // if (videoRef.current) {
// // videoRef.current.srcObject = stream;
// // }
// })
// .catch(err => console.error("Failed to get local stream", err));
// // Initialize PeerJS
// // const peer = new Peer(); // PeerJS manages server connection
// // currentPeer.current = peer;
// peer.on('open', (id) => {
// setPeerId(id);
// console.log('My peer ID is:', id);
// });
// // Listen for incoming calls
// peer.on('call', (call) => {
// if (myStream) {
// // Answer the call with your stream
// call.answer(myStream);
// call.on('stream', (remoteStream) => {
// // Show stream in remote video element
// if (videoRef.current) {
// videoRef.current.srcObject = remoteStream;
// }
// });
// }
// });
// peer.on('error', (err) => console.error(err));
// return () => {
// if (peer) {
// peer.destroy();
// }
// };
// }, [myStream]);
// useEffect(() => {
// const load = async () => {
// // Load media devices
// const res = await navigator.mediaDevices.getUserMedia({ video: true, audio: true })
// // .then(async res =>
// setDevices(await navigator.mediaDevices.enumerateDevices())
// // )
// // setFlowIdx(1)
// // setFlowIdx(2)
// // console.log(await navigator.mediaDevices.enumerateDevices())
// // console.log(await navigator.mediaDevices.getUserMedia().)
// }
// load()
// peer.on("call", function (call) {
// alert(`You have a call`)
// // Answer the call, providing our mediaStream
// // if (mediaStream) {
// call.answer(mediaStream);
// call.on('stream', (remoteStream) => {
// // Show stream in remote video element
// if (videoRef.current) {
// videoRef.current.srcObject = remoteStream;
// }
// });
// // }
// });
// }, [])
return (
<div>
{JSON.stringify(peerId)}
<div>
<b>Settings</b>
<b>Video</b>
{devices.filter(a => a.kind === 'videoinput').map(device =>
<div key={device.deviceId}
onClick={() => setSelectedDevice((d: any) => {
return { ...d, video: { deviceId: { exact: device.deviceId } } }
})}
className={`${selectedDevice && selectedDevice.video && device.deviceId === selectedDevice?.video.deviceId.exact ? 'bg-green-300' : ''}`}
>
{device.label}
</div>
)}
<b>Audio</b>
{devices.filter(a => a.kind === 'audioinput').map(device =>
<div key={device.deviceId}
onClick={() => setSelectedDevice((d: any) => {
return { ...d, audio: { deviceId: { exact: device.deviceId } } }
})}
className={`${selectedDevice && selectedDevice.audio && device.deviceId === selectedDevice?.audio.deviceId.exact ? 'bg-green-300' : ''}`}
>
{device.label}
</div>
)}
</div>
<input placeholder='Cannect to' onChange={e => setTheirPeerId(e.target.value)} />
<button onClick={() => {
console.log(`Connecting`)
connection.current
= peer.connect(theirPeerId, {
});
}}>Start</button>
<button
className='p-2'
onClick={() => {
// const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
navigator.mediaDevices.getUserMedia(selectedDevice)
.then(stream => peer.call(theirPeerId, stream))
// const call = peer.call(theirPeerId, stream);
// const stream = await navigator.mediaDevices.getUserMedia({
// video: { deviceId: { exact: selectedVideoId } }
// });
}}
>Call</button>
<textarea onChange={e => connection.current?.send(e.target.value)} placeholder='Message' rows={6} />
{JSON.stringify(received)}
<video ref={videoRef} />
</div>
)
}
@@ -0,0 +1,530 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import { CameraIcon, HdIcon, MicrochipIcon, Server, VideoIcon } from 'lucide-react';
import Peer, { DataConnection } from 'peerjs';
import React, { ReactNode, useCallback, useEffect, useRef, useState } from 'react'
type FlowStateType = {
color: 'red' | 'blue' | 'green' | 'orange',
flow: 'MIC' | 'VIDEO' | 'SERVER' | 'NO_PEERS' | 'ROOM',
loading: boolean
}
export default function WebRTCChatUsingPeer() {
let mediaStream
// UI
// const [flowSection, setFlowSection] = useState<'MIC' | 'VIDEO' | 'SERVER'>('MIC')
const [flowState, setFlowState] = useState<FlowStateType>({
color: 'blue',
flow: 'SERVER',
loading: false
})
// const [flow, setFlow] = useState<{
// screen: ReactNode,
// loading: boolean,
// color: 'red' | 'blue' | 'green' | 'orange',
// options?: any
// }[]>([
// // {
// // loading: false,
// // screen: <SelectMicrophone setLoading={setLoading} />,
// // color: 'orange',
// // options: [{
// // label: 'Next',
// // onSelect: (a: any) => { }
// // }]
// // },
// {
// loading: false,
// screen: <>
// <CameraIcon size={45} />
// Select video device
// <select>
// <option>Select</option>
// </select>
// </>,
// color: 'orange'
// }, {
// loading: true,
// screen: <ConnectingToSerever />,
// color: 'blue'
// }])
const peerRef = useRef<Peer>(undefined)
const [peers, setPeers] = useState<string[]>([])
const connection = useRef<DataConnection>(undefined)
const videoRef = useRef<HTMLVideoElement>(null)
// const [mediaStream, setMediaStream] = useState<MediaStream>(null)
const [received, setReceivved] = useState<string>('')
const [peerId, setPeerId] = useState<string>('')
const [theirPeerId, setTheirPeerId] = useState<string>('')
const [myStream, setMyStream] = useState<MediaStream>(null);
const [devices, setDevices] = useState<MediaDeviceInfo[]>([])
const [selectedDevice, setSelectedDevice] = useState<{ video: { deviceId: { exact: string } }, audio: { deviceId: { exact: string } } }>()
const handleFindPeers = () => {
peerRef.current?.listAllPeers(peers => {
setPeers(() => peers)
console.log(`PEERS`, peers)
if (peers.length > 1) {
console.log(`CONNECT TO: `, peers.filter(a => a !== peerId)[0])
if (!connection.current)
peerRef.current?.connect(peers.filter(a => a !== peerId)[0])
setFlowState(s => { return { ...s, flow: 'ROOM', color: 'blue', loading: true } })
} else {
setFlowState(s => { return { ...s, flow: 'NO_PEERS', color: 'blue', loading: true } })
}
})
}
const handleConnectServer = () => {
setFlowState(s => { return { ...s, flow: 'SERVER' } })
peerRef.current = new Peer({
host: "localhost",
port: 4000,
path: "/myapp",
secure: false, // Use true with HTTPS
})
// peerRef.current.socket.on('message')
peerRef.current.on("open", function (id) {
setPeerId(id)
setFlowState(s => { return { ...s, color: 'green', loading: false } })
console.log("My peer ID is: " + id, 'Connected Peers:',);
handleFindPeers()
});
peerRef.current.on('error', function (err) {
setFlowState(s => { return { ...s, color: 'red', loading: false } })
})
peerRef.current.on('connection', function (conn) {
console.log(`Got Connection`, conn)
connection.current = conn
// handleFindPeers()
setFlowState(s => { return { ...s, flow: 'ROOM' } })
connection.current.on("data", function (data) {
// setReceivved(data as string)
console.log("Received", data);
});
connection.current.send("Hello!");
});
}
useEffect(() => {
handleConnectServer()
}, [])
// const peer = new Peer({
// host: "localhost",
// port: 4000,
// path: "/myapp",
// secure: false, // Use true with HTTPS
// });
// useEffect(() => {
// peer.on("open", function (id) {
// setPeerId(id)
// console.log("My peer ID is: " + id);
// });
// }, [])
// peer.on('connection', function (conn) {
// console.log(`Got Connection`, conn)
// conn.on("data", function (data) {
// setReceivved(data as string)
// console.log("Received", data);
// });
// conn.send("Hello!");
// });
// useEffect(() => {
// // Get user media
// navigator.mediaDevices.getUserMedia({ video: true, audio: true })
// .then(stream => {
// // setMyStream(stream);
// // if (videoRef.current) {
// // videoRef.current.srcObject = stream;
// // }
// })
// .catch(err => console.error("Failed to get local stream", err));
// // Initialize PeerJS
// // const peer = new Peer(); // PeerJS manages server connection
// // currentPeer.current = peer;
// peer.on('open', (id) => {
// setPeerId(id);
// console.log('My peer ID is:', id);
// });
// // Listen for incoming calls
// peer.on('call', (call) => {
// if (myStream) {
// // Answer the call with your stream
// call.answer(myStream);
// call.on('stream', (remoteStream) => {
// // Show stream in remote video element
// if (videoRef.current) {
// videoRef.current.srcObject = remoteStream;
// }
// });
// }
// });
// peer.on('error', (err) => console.error(err));
// return () => {
// if (peer) {
// peer.destroy();
// }
// };
// }, [myStream]);
// useEffect(() => {
// const load = async () => {
// // Load media devices
// const res = await navigator.mediaDevices.getUserMedia({ video: true, audio: true })
// // .then(async res =>
// setDevices(await navigator.mediaDevices.enumerateDevices())
// // )
// // setFlowIdx(1)
// // setFlowIdx(2)
// // console.log(await navigator.mediaDevices.enumerateDevices())
// // console.log(await navigator.mediaDevices.getUserMedia().)
// }
// load()
// peer.on("call", function (call) {
// alert(`You have a call`)
// // Answer the call, providing our mediaStream
// // if (mediaStream) {
// call.answer(mediaStream);
// call.on('stream', (remoteStream) => {
// // Show stream in remote video element
// if (videoRef.current) {
// videoRef.current.srcObject = remoteStream;
// }
// });
// // }
// });
// }, [])
if (flowState.flow === 'ROOM') {
return <div className='flex w-screen h-screen'>
<div className='grow'></div>
<form onSubmit={(form) => {
form.preventDefault()
const formData = new FormData(form.target);
// const det = Object.fromEntries(formData.entries())
try {
console.log(`Sending:`, connection.current, formData.get('message') as string)
connection.current?.send(formData.get('message') as string)
} catch (e) {
console.error('Failed to send message', e)
}
}}>
<input name='message' placeholder='message' />
<button type='submit'>Send</button>
</form>
</div>
}
return (<Screen
color={flowState.color}
loading={flowState.loading}>
{
{
'MIC': <SelectMicrophone setFlowState={setFlowState}
onChange={e => {
setSelectedDevice((d: any) => {
return { ...d, audio: { deviceId: { exact: e } } }
})
setFlowState(s => { return { ...s, flow: 'VIDEO' } })
// setFlowSection('VIDEO')
}}
/>,
'VIDEO': <SelectVideo setFlowState={setFlowState}
onChange={e => {
setSelectedDevice((d: any) => {
return { ...d, video: { deviceId: { exact: e } } }
})
handleConnectServer()
}}
/>,
'SERVER': <ConnectingToSerever
setFlowState={setFlowState}
/>,
'NO_PEERS': <>Waiting for others to join</>,
'ROOM': <></>
}[flowState.flow]
}
{/* {flow[flowIdx].screen} */}
</Screen>)
// return (
// <div>
// {JSON.stringify(peerId)}
// <div>
// <b>Settings</b>
// <b>Video</b>
// {devices.filter(a => a.kind === 'videoinput').map(device =>
// <div key={device.deviceId}
// onClick={() => setSelectedDevice((d: any) => {
// return { ...d, video: { deviceId: { exact: device.deviceId } } }
// })}
// className={`${selectedDevice && selectedDevice.video && device.deviceId === selectedDevice?.video.deviceId.exact ? 'bg-green-300' : ''}`}
// >
// {device.label}
// </div>
// )}
// <b>Audio</b>
// {devices.filter(a => a.kind === 'audioinput').map(device =>
// <div key={device.deviceId}
// onClick={() => setSelectedDevice((d: any) => {
// return { ...d, audio: { deviceId: { exact: device.deviceId } } }
// })}
// className={`${selectedDevice && selectedDevice.audio && device.deviceId === selectedDevice?.audio.deviceId.exact ? 'bg-green-300' : ''}`}
// >
// {device.label}
// </div>
// )}
// </div>
// <input placeholder='Cannect to' onChange={e => setTheirPeerId(e.target.value)} />
// <button onClick={() => {
// console.log(`Connecting`)
// connection.current
// = peer.connect(theirPeerId, {
// });
// }}>Start</button>
// <button
// className='p-2'
// onClick={() => {
// // const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
// navigator.mediaDevices.getUserMedia(selectedDevice)
// .then(stream => peer.call(theirPeerId, stream))
// // const call = peer.call(theirPeerId, stream);
// // const stream = await navigator.mediaDevices.getUserMedia({
// // video: { deviceId: { exact: selectedVideoId } }
// // });
// }}
// >Call</button>
// <textarea onChange={e => connection.current?.send(e.target.value)} placeholder='Message' rows={6} />
// {JSON.stringify(received)}
// <video ref={videoRef} />
// </div>
// )
}
export function Screen({ color, loading, children }: {
color: 'red' | 'blue' | 'green' | 'orange',
loading: boolean
children: ReactNode
}) {
return <div
style={{
background: color == 'orange' ? '#441306' : color == 'green' ? '#032e15' : color == 'red' ? '#460809' : '#162456'
}}
className={`h-screen w-screen flex items-center align-middle justify-center`}>
<>
<div className='relative '>
{/* <div className='absolute flex items-center align-middle justify-center -left-20 -top-10 bg-blue-900 hover:bg-blue-800 shadow shadow-white shadow-2xl rounded-full grow w-64 h-64'>
ITEM 1
</div>
<div className='absolute right-0 bg-blue-950 hover:bg-blue-900 w-64 h-64'>
ITEM 2
</div>
<div className='absolute bottom-0 bg-blue-950 hover:bg-blue-900'>
ITEM 3
</div> */}
<div
style={{
background: color == 'orange' ?
'radial-gradient(circle, oklch(40.8% 0.123 38.172) 0%, rgba(0,0,0,0.3) 100%)'
: color == 'green' ?
'radial-gradient(circle, oklch(39.3% 0.095 152.535) 0%, rgba(0,0,0,0.3) 100%)' :
color == 'red' ?
'radial-gradient(circle,oklch(39.6% 0.141 25.723) 0%, rgba(0,0,0,0.3) 100%)'
:
'radial-gradient(circle,oklch(37.9% 0.146 265.522) 0%, rgba(0,0,0,0.3) 100%)'
}}
className={`w-96 h-95 rounded-full relative border-8 shadow-lg ${loading && `animate-spin shadow-purple-300/75`} text-3xl text-white flex items-center justify-center align-middle `}>
<div className="absolute inset-0 flex items-center justify-center animate-[inherit] direction-reverse">
<span className="flex gap-2 flex-col items-center font-bold text-white text-shadow-white text-shadow-2xs">
{children}
</span>
</div>
</div>
</div>
</>
</div>
}
export function ConnectingToSerever({
setFlowState,
}: {
setFlowState: React.Dispatch<React.SetStateAction<FlowStateType>>
}) {
useEffect(() => { setFlowState(s => { return { ...s, color: 'blue', loading: true } }) }, [])
// const peerConn = useCallback(() => {
// const peer = new Peer({
// host: "localhost",
// port: 4000,
// path: "/myapp",
// secure: false, // Use true with HTTPS
// });
// peer.on("open", function (id) {
// setPeerId(id)
// setColor('green')
// console.log("My peer ID is: " + id);
// });
// peer.on('error', function (err) {
// setColor('red')
// setLoading(false)
// })
// peer.on('connection', function (conn) {
// console.log(`Got Connection`, conn)
// conn.on("data", function (data) {
// // setReceivved(data as string)
// console.log("Received", data);
// });
// conn.send("Hello!");
// });
// }, [])
// useEffect(() => {
// const load = async () => {
// peer.on("open", function (id) {
// setPeerId(id)
// setColor('green')
// console.log("My peer ID is: " + id);
// });
// peer.on('error', function (err) {
// setColor('red')
// setLoading(false)
// })
// peer.on('connection', function (conn) {
// console.log(`Got Connection`, conn)
// conn.on("data", function (data) {
// // setReceivved(data as string)
// console.log("Received", data);
// });
// conn.send("Hello!");
// });
// }
// setColor('green')
// setLoading(true)
// load()
// }, [])
return <>
<Server size={45} />
Connecting to Server
</>
}
export function SelectMicrophone({
setFlowState,
onChange
}: {
setFlowState: React.Dispatch<React.SetStateAction<FlowStateType>>
onChange: (device: string) => void
}) {
const [devices, setDevices] = useState<MediaDeviceInfo[]>([])
useEffect(() => {
const load = async () => {
// Load media devices
// setTimeout(() => setLoading(false), 3000)
// setLoading(false)
const res = await navigator.mediaDevices.getUserMedia({ video: true, audio: true })
// // .then(async res =>
setDevices(await navigator.mediaDevices.enumerateDevices())
setFlowState(s => { return { ...s, color: 'orange', loading: false } })
}
setFlowState(s => { return { ...s, color: 'green', loading: true } })
load()
}, [])
return <>
<MicrochipIcon size={45} />
{
devices.length == 0 ? 'Finding Audio Devices' : <>
Select audio device
<select className='text-sm w-64 bg-white p-2 text-black'
onChange={e => onChange(e.target.value)}
>
<option>Select</option>
{devices.filter(a => a.kind === 'audioinput').map((device, i) =>
<option key={`device-${i}`} value={device.deviceId}>{device.label}</option>
)}
</select>
</>
}
</>
}
export function SelectVideo({
setFlowState,
onChange
}: {
setFlowState: React.Dispatch<React.SetStateAction<FlowStateType>>
onChange: (device: string) => void
}) {
const [devices, setDevices] = useState<MediaDeviceInfo[]>([])
useEffect(() => {
const load = async () => {
// Load media devices
// setTimeout(() => setLoading(false), 3000)
// setLoading(false)
const res = await navigator.mediaDevices.getUserMedia({ video: true, audio: true })
// // .then(async res =>
setDevices(await navigator.mediaDevices.enumerateDevices())
setFlowState(s => { return { ...s, color: 'orange', loading: false } })
}
setFlowState(s => { return { ...s, color: 'green', loading: true } })
load()
}, [])
return <>
<VideoIcon size={45} />
{
devices.length == 0 ? 'Finding Video Devices' : <>
Select video device
<select className='text-sm w-64 bg-white p-2 text-black'
onChange={e => onChange(e.target.value)}
>
<option>Select</option>
{devices.filter(a => a.kind === 'videoinput').map((device, i) =>
<option key={`device-${i}`} value={device.deviceId}>{device.label}</option>
)}
</select>
</>
}
</>
}
@@ -0,0 +1,506 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import { CameraIcon, HdIcon, MicrochipIcon, Server, VideoIcon } from 'lucide-react';
import Peer, { DataConnection } from 'peerjs';
import React, { ReactNode, useCallback, useEffect, useRef, useState } from 'react'
type FlowStateType = {
color: 'red' | 'blue' | 'green' | 'orange',
flow: 'MIC' | 'VIDEO' | 'SERVER' | 'NO_PEERS' | 'ROOM',
loading: boolean
}
export default function WebRTCChatUsingPeer() {
let mediaStream
// UI
// const [flowSection, setFlowSection] = useState<'MIC' | 'VIDEO' | 'SERVER'>('MIC')
const [flowState, setFlowState] = useState<FlowStateType>({
color: 'blue',
flow: 'SERVER',
loading: false
})
// const [flow, setFlow] = useState<{
// screen: ReactNode,
// loading: boolean,
// color: 'red' | 'blue' | 'green' | 'orange',
// options?: any
// }[]>([
// // {
// // loading: false,
// // screen: <SelectMicrophone setLoading={setLoading} />,
// // color: 'orange',
// // options: [{
// // label: 'Next',
// // onSelect: (a: any) => { }
// // }]
// // },
// {
// loading: false,
// screen: <>
// <CameraIcon size={45} />
// Select video device
// <select>
// <option>Select</option>
// </select>
// </>,
// color: 'orange'
// }, {
// loading: true,
// screen: <ConnectingToSerever />,
// color: 'blue'
// }])
const peerRef = useRef<Peer>(undefined)
const [peers, setPeers] = useState<string[]>([])
const connection = useRef<DataConnection>(undefined)
const videoRef = useRef<HTMLVideoElement>(null)
// const [mediaStream, setMediaStream] = useState<MediaStream>(null)
const [received, setReceivved] = useState<string>('')
const [peerId, setPeerId] = useState<string>('')
const [theirPeerId, setTheirPeerId] = useState<string>('')
const [myStream, setMyStream] = useState<MediaStream>(null);
const [devices, setDevices] = useState<MediaDeviceInfo[]>([])
const [selectedDevice, setSelectedDevice] = useState<{ video: { deviceId: { exact: string } }, audio: { deviceId: { exact: string } } }>()
const handleFindPeers = () => {
peerRef.current?.listAllPeers(peers => {
setPeers(() => peers)
console.log(`PEERS`, peers)
if (peers.length > 1) {
peerRef.current?.connect(peers[1])
setFlowState(s => { return { ...s, flow: 'ROOM', color: 'blue', loading: true } })
} else {
setFlowState(s => { return { ...s, flow: 'NO_PEERS', color: 'blue', loading: true } })
}
})
}
const handleConnectServer = () => {
setFlowState(s => { return { ...s, flow: 'SERVER' } })
peerRef.current = new Peer({
host: "localhost",
port: 4000,
path: "/myapp",
secure: false, // Use true with HTTPS
})
// peerRef.current.socket.on('message')
peerRef.current.on("open", function (id) {
setPeerId(id)
setFlowState(s => { return { ...s, color: 'green', loading: false } })
console.log("My peer ID is: " + id, 'Connected Peers:',);
handleFindPeers()
});
peerRef.current.on('error', function (err) {
setFlowState(s => { return { ...s, color: 'red', loading: false } })
})
peerRef.current.on('connection', function (conn) {
console.log(`Got Connection`, conn)
conn.on("data", function (data) {
// setReceivved(data as string)
console.log("Received", data);
});
conn.send("Hello!");
});
}
useEffect(() => {
handleConnectServer()
}, [])
// const peer = new Peer({
// host: "localhost",
// port: 4000,
// path: "/myapp",
// secure: false, // Use true with HTTPS
// });
// useEffect(() => {
// peer.on("open", function (id) {
// setPeerId(id)
// console.log("My peer ID is: " + id);
// });
// }, [])
// peer.on('connection', function (conn) {
// console.log(`Got Connection`, conn)
// conn.on("data", function (data) {
// setReceivved(data as string)
// console.log("Received", data);
// });
// conn.send("Hello!");
// });
// useEffect(() => {
// // Get user media
// navigator.mediaDevices.getUserMedia({ video: true, audio: true })
// .then(stream => {
// // setMyStream(stream);
// // if (videoRef.current) {
// // videoRef.current.srcObject = stream;
// // }
// })
// .catch(err => console.error("Failed to get local stream", err));
// // Initialize PeerJS
// // const peer = new Peer(); // PeerJS manages server connection
// // currentPeer.current = peer;
// peer.on('open', (id) => {
// setPeerId(id);
// console.log('My peer ID is:', id);
// });
// // Listen for incoming calls
// peer.on('call', (call) => {
// if (myStream) {
// // Answer the call with your stream
// call.answer(myStream);
// call.on('stream', (remoteStream) => {
// // Show stream in remote video element
// if (videoRef.current) {
// videoRef.current.srcObject = remoteStream;
// }
// });
// }
// });
// peer.on('error', (err) => console.error(err));
// return () => {
// if (peer) {
// peer.destroy();
// }
// };
// }, [myStream]);
// useEffect(() => {
// const load = async () => {
// // Load media devices
// const res = await navigator.mediaDevices.getUserMedia({ video: true, audio: true })
// // .then(async res =>
// setDevices(await navigator.mediaDevices.enumerateDevices())
// // )
// // setFlowIdx(1)
// // setFlowIdx(2)
// // console.log(await navigator.mediaDevices.enumerateDevices())
// // console.log(await navigator.mediaDevices.getUserMedia().)
// }
// load()
// peer.on("call", function (call) {
// alert(`You have a call`)
// // Answer the call, providing our mediaStream
// // if (mediaStream) {
// call.answer(mediaStream);
// call.on('stream', (remoteStream) => {
// // Show stream in remote video element
// if (videoRef.current) {
// videoRef.current.srcObject = remoteStream;
// }
// });
// // }
// });
// }, [])
return (<Screen
color={flowState.color}
loading={flowState.loading}>
{
{
'MIC': <SelectMicrophone setFlowState={setFlowState}
onChange={e => {
setSelectedDevice((d: any) => {
return { ...d, audio: { deviceId: { exact: e } } }
})
setFlowState(s => { return { ...s, flow: 'VIDEO' } })
// setFlowSection('VIDEO')
}}
/>,
'VIDEO': <SelectVideo setFlowState={setFlowState}
onChange={e => {
setSelectedDevice((d: any) => {
return { ...d, video: { deviceId: { exact: e } } }
})
handleConnectServer()
}}
/>,
'SERVER': <ConnectingToSerever
setFlowState={setFlowState}
/>,
'NO_PEERS': <>Waiting for others to join</>,
'ROOM': <></>
}[flowState.flow]
}
{/* {flow[flowIdx].screen} */}
</Screen>)
// return (
// <div>
// {JSON.stringify(peerId)}
// <div>
// <b>Settings</b>
// <b>Video</b>
// {devices.filter(a => a.kind === 'videoinput').map(device =>
// <div key={device.deviceId}
// onClick={() => setSelectedDevice((d: any) => {
// return { ...d, video: { deviceId: { exact: device.deviceId } } }
// })}
// className={`${selectedDevice && selectedDevice.video && device.deviceId === selectedDevice?.video.deviceId.exact ? 'bg-green-300' : ''}`}
// >
// {device.label}
// </div>
// )}
// <b>Audio</b>
// {devices.filter(a => a.kind === 'audioinput').map(device =>
// <div key={device.deviceId}
// onClick={() => setSelectedDevice((d: any) => {
// return { ...d, audio: { deviceId: { exact: device.deviceId } } }
// })}
// className={`${selectedDevice && selectedDevice.audio && device.deviceId === selectedDevice?.audio.deviceId.exact ? 'bg-green-300' : ''}`}
// >
// {device.label}
// </div>
// )}
// </div>
// <input placeholder='Cannect to' onChange={e => setTheirPeerId(e.target.value)} />
// <button onClick={() => {
// console.log(`Connecting`)
// connection.current
// = peer.connect(theirPeerId, {
// });
// }}>Start</button>
// <button
// className='p-2'
// onClick={() => {
// // const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
// navigator.mediaDevices.getUserMedia(selectedDevice)
// .then(stream => peer.call(theirPeerId, stream))
// // const call = peer.call(theirPeerId, stream);
// // const stream = await navigator.mediaDevices.getUserMedia({
// // video: { deviceId: { exact: selectedVideoId } }
// // });
// }}
// >Call</button>
// <textarea onChange={e => connection.current?.send(e.target.value)} placeholder='Message' rows={6} />
// {JSON.stringify(received)}
// <video ref={videoRef} />
// </div>
// )
}
export function Screen({ color, loading, children }: {
color: 'red' | 'blue' | 'green' | 'orange',
loading: boolean
children: ReactNode
}) {
return <div
style={{
background: color == 'orange' ? '#441306' : color == 'green' ? '#032e15' : color == 'red' ? '#460809' : '#162456'
}}
className={`h-screen w-screen flex items-center align-middle justify-center`}>
<>
<div className='relative '>
{/* <div className='absolute flex items-center align-middle justify-center -left-20 -top-10 bg-blue-900 hover:bg-blue-800 shadow shadow-white shadow-2xl rounded-full grow w-64 h-64'>
ITEM 1
</div>
<div className='absolute right-0 bg-blue-950 hover:bg-blue-900 w-64 h-64'>
ITEM 2
</div>
<div className='absolute bottom-0 bg-blue-950 hover:bg-blue-900'>
ITEM 3
</div> */}
<div
style={{
background: color == 'orange' ?
'radial-gradient(circle, oklch(40.8% 0.123 38.172) 0%, rgba(0,0,0,0.3) 100%)'
: color == 'green' ?
'radial-gradient(circle, oklch(39.3% 0.095 152.535) 0%, rgba(0,0,0,0.3) 100%)' :
color == 'red' ?
'radial-gradient(circle,oklch(39.6% 0.141 25.723) 0%, rgba(0,0,0,0.3) 100%)'
:
'radial-gradient(circle,oklch(37.9% 0.146 265.522) 0%, rgba(0,0,0,0.3) 100%)'
}}
className={`w-96 h-95 rounded-full relative border-8 shadow-lg ${loading && `animate-spin shadow-purple-300/75`} text-3xl text-white flex items-center justify-center align-middle `}>
<div className="absolute inset-0 flex items-center justify-center animate-[inherit] direction-reverse">
<span className="flex gap-2 flex-col items-center font-bold text-white text-shadow-white text-shadow-2xs">
{children}
</span>
</div>
</div>
</div>
</>
</div>
}
export function ConnectingToSerever({
setFlowState,
}: {
setFlowState: React.Dispatch<React.SetStateAction<FlowStateType>>
}) {
useEffect(() => { setFlowState(s => { return { ...s, color: 'blue', loading: true } }) }, [])
// const peerConn = useCallback(() => {
// const peer = new Peer({
// host: "localhost",
// port: 4000,
// path: "/myapp",
// secure: false, // Use true with HTTPS
// });
// peer.on("open", function (id) {
// setPeerId(id)
// setColor('green')
// console.log("My peer ID is: " + id);
// });
// peer.on('error', function (err) {
// setColor('red')
// setLoading(false)
// })
// peer.on('connection', function (conn) {
// console.log(`Got Connection`, conn)
// conn.on("data", function (data) {
// // setReceivved(data as string)
// console.log("Received", data);
// });
// conn.send("Hello!");
// });
// }, [])
// useEffect(() => {
// const load = async () => {
// peer.on("open", function (id) {
// setPeerId(id)
// setColor('green')
// console.log("My peer ID is: " + id);
// });
// peer.on('error', function (err) {
// setColor('red')
// setLoading(false)
// })
// peer.on('connection', function (conn) {
// console.log(`Got Connection`, conn)
// conn.on("data", function (data) {
// // setReceivved(data as string)
// console.log("Received", data);
// });
// conn.send("Hello!");
// });
// }
// setColor('green')
// setLoading(true)
// load()
// }, [])
return <>
<Server size={45} />
Connecting to Server
</>
}
export function SelectMicrophone({
setFlowState,
onChange
}: {
setFlowState: React.Dispatch<React.SetStateAction<FlowStateType>>
onChange: (device: string) => void
}) {
const [devices, setDevices] = useState<MediaDeviceInfo[]>([])
useEffect(() => {
const load = async () => {
// Load media devices
// setTimeout(() => setLoading(false), 3000)
// setLoading(false)
const res = await navigator.mediaDevices.getUserMedia({ video: true, audio: true })
// // .then(async res =>
setDevices(await navigator.mediaDevices.enumerateDevices())
setFlowState(s => { return { ...s, color: 'orange', loading: false } })
}
setFlowState(s => { return { ...s, color: 'green', loading: true } })
load()
}, [])
return <>
<MicrochipIcon size={45} />
{
devices.length == 0 ? 'Finding Audio Devices' : <>
Select audio device
<select className='text-sm w-64 bg-white p-2 text-black'
onChange={e => onChange(e.target.value)}
>
<option>Select</option>
{devices.filter(a => a.kind === 'audioinput').map((device, i) =>
<option key={`device-${i}`} value={device.deviceId}>{device.label}</option>
)}
</select>
</>
}
</>
}
export function SelectVideo({
setFlowState,
onChange
}: {
setFlowState: React.Dispatch<React.SetStateAction<FlowStateType>>
onChange: (device: string) => void
}) {
const [devices, setDevices] = useState<MediaDeviceInfo[]>([])
useEffect(() => {
const load = async () => {
// Load media devices
// setTimeout(() => setLoading(false), 3000)
// setLoading(false)
const res = await navigator.mediaDevices.getUserMedia({ video: true, audio: true })
// // .then(async res =>
setDevices(await navigator.mediaDevices.enumerateDevices())
setFlowState(s => { return { ...s, color: 'orange', loading: false } })
}
setFlowState(s => { return { ...s, color: 'green', loading: true } })
load()
}, [])
return <>
<VideoIcon size={45} />
{
devices.length == 0 ? 'Finding Video Devices' : <>
Select video device
<select className='text-sm w-64 bg-white p-2 text-black'
onChange={e => onChange(e.target.value)}
>
<option>Select</option>
{devices.filter(a => a.kind === 'videoinput').map((device, i) =>
<option key={`device-${i}`} value={device.deviceId}>{device.label}</option>
)}
</select>
</>
}
</>
}
@@ -0,0 +1,538 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import { CameraIcon, HdIcon, MicrochipIcon, Server, VideoIcon } from 'lucide-react';
import Peer, { DataConnection } from 'peerjs';
import React, { ReactNode, useCallback, useEffect, useRef, useState } from 'react'
type FlowStateType = {
color: 'red' | 'blue' | 'green' | 'orange',
flow: 'MIC' | 'VIDEO' | 'SERVER' | 'NO_PEERS' | 'ROOM',
loading: boolean
}
export default function WebRTCChatUsingPeer() {
let mediaStream
// UI
// const [flowSection, setFlowSection] = useState<'MIC' | 'VIDEO' | 'SERVER'>('MIC')
const [flowState, setFlowState] = useState<FlowStateType>({
color: 'blue',
flow: 'SERVER',
loading: false
})
// const [flow, setFlow] = useState<{
// screen: ReactNode,
// loading: boolean,
// color: 'red' | 'blue' | 'green' | 'orange',
// options?: any
// }[]>([
// // {
// // loading: false,
// // screen: <SelectMicrophone setLoading={setLoading} />,
// // color: 'orange',
// // options: [{
// // label: 'Next',
// // onSelect: (a: any) => { }
// // }]
// // },
// {
// loading: false,
// screen: <>
// <CameraIcon size={45} />
// Select video device
// <select>
// <option>Select</option>
// </select>
// </>,
// color: 'orange'
// }, {
// loading: true,
// screen: <ConnectingToSerever />,
// color: 'blue'
// }])
const peerRef = useRef<Peer>(undefined)
const [peers, setPeers] = useState<string[]>([])
const connection = useRef<DataConnection>(undefined)
const videoRef = useRef<HTMLVideoElement>(null)
// const [mediaStream, setMediaStream] = useState<MediaStream>(null)
const [received, setReceivved] = useState<string>('')
const [peerId, setPeerId] = useState<string>('')
const [removepeerId, setRemovePeerId] = useState<string>('')
const [theirPeerId, setTheirPeerId] = useState<string>('')
const [myStream, setMyStream] = useState<MediaStream|null>(null);
const [devices, setDevices] = useState<MediaDeviceInfo[]>([])
const [selectedDevice, setSelectedDevice] = useState<{ video: { deviceId: { exact: string } }, audio: { deviceId: { exact: string } } }>()
const handleFindPeers = () => {
peerRef.current?.listAllPeers(peers => {
setPeers(() => peers)
console.log(`PEERS`, peers)
if (peers.length > 1) {
console.log(`CONNECT TO: `, peers.filter(a => a !== peerId)[0])
if (!connection.current) {
connection.current = peerRef.current?.connect(peers.filter(a => a !== peerId)[0])
setRemovePeerId(() => peers.filter(a => a !== peerId)[0])
}
setFlowState(s => { return { ...s, flow: 'ROOM', color: 'blue', loading: true } })
} else {
setFlowState(s => { return { ...s, flow: 'NO_PEERS', color: 'blue', loading: true } })
}
})
}
const handleConnectServer = () => {
setFlowState(s => { return { ...s, flow: 'SERVER' } })
peerRef.current = new Peer({
host: "localhost",
port: 4000,
path: "/myapp",
secure: false, // Use true with HTTPS
})
// peerRef.current.socket.on('message')
peerRef.current.on("open", function (id) {
setPeerId(id)
setFlowState(s => { return { ...s, color: 'green', loading: false } })
console.log("My peer ID is: " + id, 'Connected Peers:',);
handleFindPeers()
});
peerRef.current.on('error', function (err) {
setFlowState(s => { return { ...s, color: 'red', loading: false } })
})
peerRef.current.on('close', function () {
console.log(`Closing connection`)
})
peerRef.current.on('connection', function (conn) {
console.log(`Got Connection`, conn)
// handleFindPeers()
setFlowState(s => { return { ...s, flow: 'ROOM' } })
connection.current!.on("data", function (data) {
// setReceivved(data as string)
console.log("Received", data);
});
conn.send("Hello!");
});
}
useEffect(() => {
handleConnectServer()
}, [])
// const peer = new Peer({
// host: "localhost",
// port: 4000,
// path: "/myapp",
// secure: false, // Use true with HTTPS
// });
// useEffect(() => {
// peer.on("open", function (id) {
// setPeerId(id)
// console.log("My peer ID is: " + id);
// });
// }, [])
// peer.on('connection', function (conn) {
// console.log(`Got Connection`, conn)
// conn.on("data", function (data) {
// setReceivved(data as string)
// console.log("Received", data);
// });
// conn.send("Hello!");
// });
// useEffect(() => {
// // Get user media
// navigator.mediaDevices.getUserMedia({ video: true, audio: true })
// .then(stream => {
// // setMyStream(stream);
// // if (videoRef.current) {
// // videoRef.current.srcObject = stream;
// // }
// })
// .catch(err => console.error("Failed to get local stream", err));
// // Initialize PeerJS
// // const peer = new Peer(); // PeerJS manages server connection
// // currentPeer.current = peer;
// peer.on('open', (id) => {
// setPeerId(id);
// console.log('My peer ID is:', id);
// });
// // Listen for incoming calls
// peer.on('call', (call) => {
// if (myStream) {
// // Answer the call with your stream
// call.answer(myStream);
// call.on('stream', (remoteStream) => {
// // Show stream in remote video element
// if (videoRef.current) {
// videoRef.current.srcObject = remoteStream;
// }
// });
// }
// });
// peer.on('error', (err) => console.error(err));
// return () => {
// if (peer) {
// peer.destroy();
// }
// };
// }, [myStream]);
// useEffect(() => {
// const load = async () => {
// // Load media devices
// const res = await navigator.mediaDevices.getUserMedia({ video: true, audio: true })
// // .then(async res =>
// setDevices(await navigator.mediaDevices.enumerateDevices())
// // )
// // setFlowIdx(1)
// // setFlowIdx(2)
// // console.log(await navigator.mediaDevices.enumerateDevices())
// // console.log(await navigator.mediaDevices.getUserMedia().)
// }
// load()
// peer.on("call", function (call) {
// alert(`You have a call`)
// // Answer the call, providing our mediaStream
// // if (mediaStream) {
// call.answer(mediaStream);
// call.on('stream', (remoteStream) => {
// // Show stream in remote video element
// if (videoRef.current) {
// videoRef.current.srcObject = remoteStream;
// }
// });
// // }
// });
// }, [])
if (flowState.flow === 'ROOM') {
return <div className='flex flex-col w-screen h-screen'>
<div className='flex justify-between w-full flex-col'>
<div>LOCAL PEER:{peerId}</div>
<div>REMOVE PEER:{removepeerId}</div>
</div>
<div className='grow'></div>
<form onSubmit={(form) => {
form.preventDefault()
const formData = new FormData(form.target);
// const det = Object.fromEntries(formData.entries())
try {
console.log(`Sending:`, connection.current, formData.get('message') as string)
connection.current?.send(formData.get('message') as string)
} catch (e) {
console.error('Failed to send message', e)
}
}}>
<input name='message' placeholder='message' />
<button type='submit'>Send</button>
</form>
</div>
}
return (<Screen
color={flowState.color}
loading={flowState.loading}>
{
{
'MIC': <SelectMicrophone setFlowState={setFlowState}
onChange={e => {
setSelectedDevice((d: any) => {
return { ...d, audio: { deviceId: { exact: e } } }
})
setFlowState(s => { return { ...s, flow: 'VIDEO' } })
// setFlowSection('VIDEO')
}}
/>,
'VIDEO': <SelectVideo setFlowState={setFlowState}
onChange={e => {
setSelectedDevice((d: any) => {
return { ...d, video: { deviceId: { exact: e } } }
})
handleConnectServer()
}}
/>,
'SERVER': <ConnectingToSerever
setFlowState={setFlowState}
/>,
'NO_PEERS': <>Waiting for others to join</>,
'ROOM': <></>
}[flowState.flow]
}
{/* {flow[flowIdx].screen} */}
</Screen>)
// return (
// <div>
// {JSON.stringify(peerId)}
// <div>
// <b>Settings</b>
// <b>Video</b>
// {devices.filter(a => a.kind === 'videoinput').map(device =>
// <div key={device.deviceId}
// onClick={() => setSelectedDevice((d: any) => {
// return { ...d, video: { deviceId: { exact: device.deviceId } } }
// })}
// className={`${selectedDevice && selectedDevice.video && device.deviceId === selectedDevice?.video.deviceId.exact ? 'bg-green-300' : ''}`}
// >
// {device.label}
// </div>
// )}
// <b>Audio</b>
// {devices.filter(a => a.kind === 'audioinput').map(device =>
// <div key={device.deviceId}
// onClick={() => setSelectedDevice((d: any) => {
// return { ...d, audio: { deviceId: { exact: device.deviceId } } }
// })}
// className={`${selectedDevice && selectedDevice.audio && device.deviceId === selectedDevice?.audio.deviceId.exact ? 'bg-green-300' : ''}`}
// >
// {device.label}
// </div>
// )}
// </div>
// <input placeholder='Cannect to' onChange={e => setTheirPeerId(e.target.value)} />
// <button onClick={() => {
// console.log(`Connecting`)
// connection.current
// = peer.connect(theirPeerId, {
// });
// }}>Start</button>
// <button
// className='p-2'
// onClick={() => {
// // const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
// navigator.mediaDevices.getUserMedia(selectedDevice)
// .then(stream => peer.call(theirPeerId, stream))
// // const call = peer.call(theirPeerId, stream);
// // const stream = await navigator.mediaDevices.getUserMedia({
// // video: { deviceId: { exact: selectedVideoId } }
// // });
// }}
// >Call</button>
// <textarea onChange={e => connection.current?.send(e.target.value)} placeholder='Message' rows={6} />
// {JSON.stringify(received)}
// <video ref={videoRef} />
// </div>
// )
}
export function Screen({ color, loading, children }: {
color: 'red' | 'blue' | 'green' | 'orange',
loading: boolean
children: ReactNode
}) {
return <div
style={{
background: color == 'orange' ? '#441306' : color == 'green' ? '#032e15' : color == 'red' ? '#460809' : '#162456'
}}
className={`h-screen w-screen flex items-center align-middle justify-center`}>
<>
<div className='relative '>
{/* <div className='absolute flex items-center align-middle justify-center -left-20 -top-10 bg-blue-900 hover:bg-blue-800 shadow shadow-white shadow-2xl rounded-full grow w-64 h-64'>
ITEM 1
</div>
<div className='absolute right-0 bg-blue-950 hover:bg-blue-900 w-64 h-64'>
ITEM 2
</div>
<div className='absolute bottom-0 bg-blue-950 hover:bg-blue-900'>
ITEM 3
</div> */}
<div
style={{
background: color == 'orange' ?
'radial-gradient(circle, oklch(40.8% 0.123 38.172) 0%, rgba(0,0,0,0.3) 100%)'
: color == 'green' ?
'radial-gradient(circle, oklch(39.3% 0.095 152.535) 0%, rgba(0,0,0,0.3) 100%)' :
color == 'red' ?
'radial-gradient(circle,oklch(39.6% 0.141 25.723) 0%, rgba(0,0,0,0.3) 100%)'
:
'radial-gradient(circle,oklch(37.9% 0.146 265.522) 0%, rgba(0,0,0,0.3) 100%)'
}}
className={`w-96 h-95 rounded-full relative border-8 shadow-lg ${loading && `animate-spin shadow-purple-300/75`} text-3xl text-white flex items-center justify-center align-middle `}>
<div className="absolute inset-0 flex items-center justify-center animate-[inherit] direction-reverse">
<span className="flex gap-2 flex-col items-center font-bold text-white text-shadow-white text-shadow-2xs">
{children}
</span>
</div>
</div>
</div>
</>
</div>
}
export function ConnectingToSerever({
setFlowState,
}: {
setFlowState: React.Dispatch<React.SetStateAction<FlowStateType>>
}) {
useEffect(() => { setFlowState(s => { return { ...s, color: 'blue', loading: true } }) }, [])
// const peerConn = useCallback(() => {
// const peer = new Peer({
// host: "localhost",
// port: 4000,
// path: "/myapp",
// secure: false, // Use true with HTTPS
// });
// peer.on("open", function (id) {
// setPeerId(id)
// setColor('green')
// console.log("My peer ID is: " + id);
// });
// peer.on('error', function (err) {
// setColor('red')
// setLoading(false)
// })
// peer.on('connection', function (conn) {
// console.log(`Got Connection`, conn)
// conn.on("data", function (data) {
// // setReceivved(data as string)
// console.log("Received", data);
// });
// conn.send("Hello!");
// });
// }, [])
// useEffect(() => {
// const load = async () => {
// peer.on("open", function (id) {
// setPeerId(id)
// setColor('green')
// console.log("My peer ID is: " + id);
// });
// peer.on('error', function (err) {
// setColor('red')
// setLoading(false)
// })
// peer.on('connection', function (conn) {
// console.log(`Got Connection`, conn)
// conn.on("data", function (data) {
// // setReceivved(data as string)
// console.log("Received", data);
// });
// conn.send("Hello!");
// });
// }
// setColor('green')
// setLoading(true)
// load()
// }, [])
return <>
<Server size={45} />
Connecting to Server
</>
}
export function SelectMicrophone({
setFlowState,
onChange
}: {
setFlowState: React.Dispatch<React.SetStateAction<FlowStateType>>
onChange: (device: string) => void
}) {
const [devices, setDevices] = useState<MediaDeviceInfo[]>([])
useEffect(() => {
const load = async () => {
// Load media devices
// setTimeout(() => setLoading(false), 3000)
// setLoading(false)
const res = await navigator.mediaDevices.getUserMedia({ video: true, audio: true })
// // .then(async res =>
setDevices(await navigator.mediaDevices.enumerateDevices())
setFlowState(s => { return { ...s, color: 'orange', loading: false } })
}
setFlowState(s => { return { ...s, color: 'green', loading: true } })
load()
}, [])
return <>
<MicrochipIcon size={45} />
{
devices.length == 0 ? 'Finding Audio Devices' : <>
Select audio device
<select className='text-sm w-64 bg-white p-2 text-black'
onChange={e => onChange(e.target.value)}
>
<option>Select</option>
{devices.filter(a => a.kind === 'audioinput').map((device, i) =>
<option key={`device-${i}`} value={device.deviceId}>{device.label}</option>
)}
</select>
</>
}
</>
}
export function SelectVideo({
setFlowState,
onChange
}: {
setFlowState: React.Dispatch<React.SetStateAction<FlowStateType>>
onChange: (device: string) => void
}) {
const [devices, setDevices] = useState<MediaDeviceInfo[]>([])
useEffect(() => {
const load = async () => {
// Load media devices
// setTimeout(() => setLoading(false), 3000)
// setLoading(false)
const res = await navigator.mediaDevices.getUserMedia({ video: true, audio: true })
// // .then(async res =>
setDevices(await navigator.mediaDevices.enumerateDevices())
setFlowState(s => { return { ...s, color: 'orange', loading: false } })
}
setFlowState(s => { return { ...s, color: 'green', loading: true } })
load()
}, [])
return <>
<VideoIcon size={45} />
{
devices.length == 0 ? 'Finding Video Devices' : <>
Select video device
<select className='text-sm w-64 bg-white p-2 text-black'
onChange={e => onChange(e.target.value)}
>
<option>Select</option>
{devices.filter(a => a.kind === 'videoinput').map((device, i) =>
<option key={`device-${i}`} value={device.deviceId}>{device.label}</option>
)}
</select>
</>
}
</>
}
+197
View File
@@ -0,0 +1,197 @@
'use client'
/* eslint-disable @typescript-eslint/no-explicit-any */
import React, { useEffect, useRef, useState } from 'react'
// https://www.baeldung.com/webrtc
export default function WebRTComponent() {
const WSRef = useRef<WebSocket>(undefined)
const videoRef1 = useRef<HTMLVideoElement | null>(null)
const localStreamRef1 = useRef<MediaStream | null>(null)
const [offers, setOffers] = useState<RTCSessionDescriptionInit[]>([])
const peerConnection = useRef<RTCPeerConnection>(undefined)
const dataChannel = useRef<RTCDataChannel>(undefined)
const send = (message: any) =>
WSRef.current!.send(JSON.stringify(message));
const onWSMessage = async (message: MessageEvent) => {
console.log(`Received : `, message.data)
const parsed = JSON.parse(message.data)
switch (parsed.type) {
case 'Offers':
// setOffers(o => parsed.data as RTCSessionDescriptionInit[])
console.log('parsed.data',parsed.data)
await acceptOffer(parsed.data)
// await acceptOffer(parsed.data[parsed.data.length - 1])
break;
case 'Answers':
console.log(`Answers:`,parsed.data)
peerConnection.current!
.setRemoteDescription(new RTCSessionDescription({ ...parsed.data.sdp }))
.catch((err) => console.error(err)); //
break;
}
}
const handleConectWS = async () => {
WSRef.current = new WebSocket('ws://localhost:4000/socket');
WSRef.current.onopen = async function (event) {
peerConnection.current = new RTCPeerConnection(undefined) // <-- Later add Stun servers as backup
await setup()
}
WSRef.current.onmessage = async (ev) => onWSMessage(ev)
}
// 1. Get user media
const Start = async () => {
console.log('Requesting local stream');
const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
console.log(`Start Local Stream`)
if (!videoRef1.current) console.error(`Video not yet available`)
videoRef1.current!.srcObject = stream
localStreamRef1.current = stream;
console.log(`Set local stream`)
}
const joinServer = async () => {
const configuration = {};
console.log('RTCPeerConnection configuration:', configuration);
peerConnection.current = new RTCPeerConnection(configuration)
peerConnection.current.createOffer().then(offer => {
send({
type: 'OfferSDP',
data: offer
})
peerConnection.current?.setLocalDescription(offer)
})
peerConnection.current.onicecandidate = function (event) {
console.log(`Received ICE Candidate :`, event)
}
// pc.addEventListener('icecandidate', e => console.log(`icecandidate`, pc, e));
}
const acceptOffer = async (recv: any) => {
console.log(`acceptOffer`, recv)
peerConnection.current!.setRemoteDescription(new RTCSessionDescription(recv))
.then(() => peerConnection.current!.createAnswer())
.then((answer) => peerConnection.current!.setLocalDescription(answer))
.then(() => send({ type: 'AnswerSDP', data: { sdp: peerConnection.current!.localDescription } }))
.catch((err) => console.error('Failed to handle offer', err));
}
// 2. Make call
const makeCall = () => {
// Get available streams
const videoTracks = localStreamRef1.current!.getVideoTracks();
const audioTracks = localStreamRef1.current!.getAudioTracks();
if (videoTracks.length > 0) {
console.log(`Using video device: ${videoTracks[0].label}`);
}
if (audioTracks.length > 0) {
console.log(`Using audio device: ${audioTracks[0].label}`);
}
const configuration = {};
// 3. Start WebRTC
console.log('RTCPeerConnection configuration:', configuration);
// const pc1 = new RTCPeerConnection(configuration);
// console.log('Created local peer connection object pc1');
// pc1.addEventListener('icecandidate', e => onIceCandidate(pc1, e));
// const pc2 = new RTCPeerConnection(configuration);
// console.log('Created remote peer connection object pc2');
// pc2.addEventListener('icecandidate', e => onIceCandidate(pc2, e));
// pc1.addEventListener('iceconnectionstatechange', e => onIceStateChange(pc1, e));
// pc2.addEventListener('iceconnectionstatechange', e => onIceStateChange(pc2, e));
// pc2.addEventListener('track', gotRemoteStream);
}
// 4. On ICE Candidate
// const onIceCandidate = async (peerConnection: RTCPeerConnection, e: Event) => {
// try {
// await(getOtherPc(pc).addIceCandidate(event.candidate));
// onAddIceCandidateSuccess(pc);
// } catch (e) {
// onAddIceCandidateError(pc, e);
// }
// console.log(`${getName(pc)} ICE candidate:\n${event.candidate ? event.candidate.candidate : '(null)'}`);
// }
async function setup() {
// 1. Get local media stream
// await Start()
// 2. Join WS Server
const handleJoin = () => {
send({ type: 'Join' })
}
handleJoin()
}
useEffect(() => {
handleConectWS()
// const load = async () => {
// WSRef.current.onmessage = (ev) => {
// // '{"event":"candidate","data":{"candidate":"candidate:1041930304 1 udp 2113937151 b7178aee-2d1d-4762-b709-1b25cf436cb5.local 61551 typ host generation 0 ufrag bEe6 network-cost 999","sdpMid":"0","sdpMLineIndex":0,"usernameFragment":"bEe6"}}'
// const data = JSON.parse(ev.data)
// // console.log(`Recevied WS : `, Object.keys(data))
// console.log(`Recevied WS : `, data.event)
// switch (data.event) {
// case 'offer':
// console.log(`Check foreign offer`, data.data)
// // Need to handle answer here
// peerConnection.current.setRemoteDescription(new RTCSessionDescription({ sdp: data.data.sdp, type: 'offer' }))
// .then(() => peerConnection.current.createAnswer())
// .then((answer) => peerConnection.current.setLocalDescription(answer))
// .then(() => send({ event: 'answer', data: { sdp: peerConnection.current.localDescription } }))
// .catch((err) => console.error('Failed to handle offer', err));
// break;
// case 'answer':
// console.log(`Check foreign answer`, data.data)
// peerConnection.current.setRemoteDescription(new RTCSessionDescription({ ...data.data.sdp })).catch((err) => console.error(err)); //
// break;
// case 'candidate':
// console.log(`Check foreign candidate`, { ...data.data })
// if (data.data.candidate) {
// peerConnection.current.addIceCandidate(new RTCIceCandidate({ ...data.data })).catch((err) => console.warn('Bad candidate', err));
// }
// break;
// }
// }
// }
// load()
}, [])
return (
<div>WebRTCChat
<video ref={videoRef1} playsInline autoPlay muted className='border w-96 h-96 bg-red-100' />
<button onClick={() => Start()}>Start</button>
<button onClick={() => joinServer()}>Join</button>
<button onClick={() => makeCall()}>Make Call</button>
<button onClick={() => {
{ alert('SEND'); dataChannel.current!.send("message"); }
}} >Send</button>
<div>
<b>Offers</b>
{/* <div className='flex flex-col'>
{offers.map((o, i) => <div key={`Offer-${i}`}>
{JSON.stringify(o.sdp)}
<button onClick={() => {
acceptOffer(o.sdp ?? "")
}}>Connect</button>
</div>)}
</div> */}
</div>
</div>
)
}
+214
View File
@@ -0,0 +1,214 @@
/*
* Copyright (c) 2015 The WebRTC project authors. All Rights Reserved.
*
* Use of this source code is governed by a BSD-style license
* that can be found in the LICENSE file in the root of the source
* tree.
*/
'use strict';
const startButton = document.getElementById('startButton');
const callButton = document.getElementById('callButton');
const hangupButton = document.getElementById('hangupButton');
callButton.disabled = true;
hangupButton.disabled = true;
startButton.addEventListener('click', start);
callButton.addEventListener('click', call);
hangupButton.addEventListener('click', hangup);
let startTime;
const localVideo = document.getElementById('localVideo');
const remoteVideo = document.getElementById('remoteVideo');
localVideo.addEventListener('loadedmetadata', function() {
console.log(`Local video videoWidth: ${this.videoWidth}px, videoHeight: ${this.videoHeight}px`);
});
remoteVideo.addEventListener('loadedmetadata', function() {
console.log(`Remote video videoWidth: ${this.videoWidth}px, videoHeight: ${this.videoHeight}px`);
});
remoteVideo.addEventListener('resize', () => {
console.log(`Remote video size changed to ${remoteVideo.videoWidth}x${remoteVideo.videoHeight} - Time since pageload ${performance.now().toFixed(0)}ms`);
// We'll use the first onsize callback as an indication that video has started
// playing out.
if (startTime) {
const elapsedTime = window.performance.now() - startTime;
console.log('Setup time: ' + elapsedTime.toFixed(3) + 'ms');
startTime = null;
}
});
let localStream;
let pc1;
let pc2;
const offerOptions = {
offerToReceiveAudio: 1,
offerToReceiveVideo: 1
};
function getName(pc) {
return (pc === pc1) ? 'pc1' : 'pc2';
}
function getOtherPc(pc) {
return (pc === pc1) ? pc2 : pc1;
}
async function start() {
console.log('Requesting local stream');
startButton.disabled = true;
try {
const stream = await navigator.mediaDevices.getUserMedia({audio: true, video: true});
console.log('Received local stream');
localVideo.srcObject = stream;
localStream = stream;
callButton.disabled = false;
} catch (e) {
alert(`getUserMedia() error: ${e.name}`);
}
}
async function call() {
callButton.disabled = true;
hangupButton.disabled = false;
console.log('Starting call');
startTime = window.performance.now();
const videoTracks = localStream.getVideoTracks();
const audioTracks = localStream.getAudioTracks();
if (videoTracks.length > 0) {
console.log(`Using video device: ${videoTracks[0].label}`);
}
if (audioTracks.length > 0) {
console.log(`Using audio device: ${audioTracks[0].label}`);
}
const configuration = {};
console.log('RTCPeerConnection configuration:', configuration);
pc1 = new RTCPeerConnection(configuration);
console.log('Created local peer connection object pc1');
pc1.addEventListener('icecandidate', e => onIceCandidate(pc1, e));
pc2 = new RTCPeerConnection(configuration);
console.log('Created remote peer connection object pc2');
pc2.addEventListener('icecandidate', e => onIceCandidate(pc2, e));
pc1.addEventListener('iceconnectionstatechange', e => onIceStateChange(pc1, e));
pc2.addEventListener('iceconnectionstatechange', e => onIceStateChange(pc2, e));
pc2.addEventListener('track', gotRemoteStream);
localStream.getTracks().forEach(track => pc1.addTrack(track, localStream));
console.log('Added local stream to pc1');
try {
console.log('pc1 createOffer start');
const offer = await pc1.createOffer(offerOptions);
await onCreateOfferSuccess(offer);
} catch (e) {
onCreateSessionDescriptionError(e);
}
}
function onCreateSessionDescriptionError(error) {
console.log(`Failed to create session description: ${error.toString()}`);
}
async function onCreateOfferSuccess(desc) {
console.log(`Offer from pc1\n${desc.sdp}`);
console.log('pc1 setLocalDescription start');
try {
await pc1.setLocalDescription(desc);
onSetLocalSuccess(pc1);
} catch (e) {
onSetSessionDescriptionError();
}
console.log('pc2 setRemoteDescription start');
try {
await pc2.setRemoteDescription(desc);
onSetRemoteSuccess(pc2);
} catch (e) {
onSetSessionDescriptionError();
}
console.log('pc2 createAnswer start');
// Since the 'remote' side has no media stream we need
// to pass in the right constraints in order for it to
// accept the incoming offer of audio and video.
try {
const answer = await pc2.createAnswer();
await onCreateAnswerSuccess(answer);
} catch (e) {
onCreateSessionDescriptionError(e);
}
}
function onSetLocalSuccess(pc) {
console.log(`${getName(pc)} setLocalDescription complete`);
}
function onSetRemoteSuccess(pc) {
console.log(`${getName(pc)} setRemoteDescription complete`);
}
function onSetSessionDescriptionError(error) {
console.log(`Failed to set session description: ${error.toString()}`);
}
function gotRemoteStream(e) {
if (remoteVideo.srcObject !== e.streams[0]) {
remoteVideo.srcObject = e.streams[0];
console.log('pc2 received remote stream');
}
}
async function onCreateAnswerSuccess(desc) {
console.log(`Answer from pc2:\n${desc.sdp}`);
console.log('pc2 setLocalDescription start');
try {
await pc2.setLocalDescription(desc);
onSetLocalSuccess(pc2);
} catch (e) {
onSetSessionDescriptionError(e);
}
console.log('pc1 setRemoteDescription start');
try {
await pc1.setRemoteDescription(desc);
onSetRemoteSuccess(pc1);
} catch (e) {
onSetSessionDescriptionError(e);
}
}
async function onIceCandidate(pc, event) {
try {
await (getOtherPc(pc).addIceCandidate(event.candidate));
onAddIceCandidateSuccess(pc);
} catch (e) {
onAddIceCandidateError(pc, e);
}
console.log(`${getName(pc)} ICE candidate:\n${event.candidate ? event.candidate.candidate : '(null)'}`);
}
function onAddIceCandidateSuccess(pc) {
console.log(`${getName(pc)} addIceCandidate success`);
}
function onAddIceCandidateError(pc, error) {
console.log(`${getName(pc)} failed to add ICE Candidate: ${error.toString()}`);
}
function onIceStateChange(pc, event) {
if (pc) {
console.log(`${getName(pc)} ICE state: ${pc.iceConnectionState}`);
console.log('ICE state change event: ', event);
}
}
function hangup() {
console.log('Ending call');
pc1.close();
pc2.close();
pc1 = null;
pc2 = null;
hangupButton.disabled = true;
callButton.disabled = false;
}
+6
View File
@@ -0,0 +1,6 @@
import { io } from 'socket.io-client';
// "undefined" means the URL will be computed from the window.location object
const URL = process.env.NODE_ENV === 'production' ? 'http://192.168.0.131:4000' : 'http://192.168.0.131:4000';
export const socket = io(URL);
+72
View File
@@ -0,0 +1,72 @@
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
} from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { LoginFormAction } from "@/actions/auth/AuthActions"
export function LoginForm({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div className={cn("flex flex-col gap-6", className)} {...props}>
<Card>
<CardHeader>
<CardTitle>Login to your account</CardTitle>
<CardDescription>
Enter your email below to login to your account
</CardDescription>
</CardHeader>
<CardContent>
<form action={LoginFormAction}>
<FieldGroup>
<Field>
<FieldLabel htmlFor="email">Email</FieldLabel>
<Input
id="email"
type="email"
name="email"
placeholder="m@example.com"
required
/>
</Field>
<Field>
<div className="flex items-center">
<FieldLabel htmlFor="password">Password</FieldLabel>
<a
href="#"
className="ml-auto inline-block text-sm underline-offset-4 hover:underline"
>
Forgot your password?
</a>
</div>
<Input id="password" type="password" name="password" required />
</Field>
<Field>
<Button type="submit">Login</Button>
<Button variant="outline" type="button">
Login with Passkey
</Button>
<FieldDescription className="text-center">
Don&apos;t have an account? <a href="/signup">Sign up</a>
</FieldDescription>
</Field>
</FieldGroup>
</form>
</CardContent>
</Card>
</div>
)
}
+76
View File
@@ -0,0 +1,76 @@
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
} from "@/components/ui/field"
import { Input } from "@/components/ui/input"
export function SignupForm({ ...props }: React.ComponentProps<typeof Card>) {
return (
<Card {...props}>
<CardHeader>
<CardTitle>Create an account</CardTitle>
<CardDescription>
Enter your information below to create your account
</CardDescription>
</CardHeader>
<CardContent>
<form>
<FieldGroup>
<Field>
<FieldLabel htmlFor="name">Full Name</FieldLabel>
<Input id="name" type="text" placeholder="John Doe" required />
</Field>
<Field>
<FieldLabel htmlFor="email">Email</FieldLabel>
<Input
id="email"
type="email"
placeholder="m@example.com"
required
/>
<FieldDescription>
We&apos;ll use this to contact you. We will not share your email
with anyone else.
</FieldDescription>
</Field>
<Field>
<FieldLabel htmlFor="password">Password</FieldLabel>
<Input id="password" type="password" required />
<FieldDescription>
Must be at least 8 characters long.
</FieldDescription>
</Field>
<Field>
<FieldLabel htmlFor="confirm-password">
Confirm Password
</FieldLabel>
<Input id="confirm-password" type="password" required />
<FieldDescription>Please confirm your password.</FieldDescription>
</Field>
<FieldGroup>
<Field>
<Button type="submit">Create Account</Button>
<Button variant="outline" type="button">
Sign up with Google
</Button>
<FieldDescription className="px-6 text-center">
Already have an account? <a href="/login">Sign in</a>
</FieldDescription>
</Field>
</FieldGroup>
</FieldGroup>
</form>
</CardContent>
</Card>
)
}
+67
View File
@@ -0,0 +1,67 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
outline:
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
ghost:
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
destructive:
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default:
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
icon: "size-8",
"icon-xs":
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
"icon-sm":
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
"icon-lg": "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant = "default",
size = "default",
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot.Root : "button"
return (
<Comp
data-slot="button"
data-variant={variant}
data-size={size}
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }
+103
View File
@@ -0,0 +1,103 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({
className,
size = "default",
...props
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
return (
<div
data-slot="card"
data-size={size}
className={cn(
"group/card flex flex-col gap-4 overflow-hidden rounded-xl bg-card py-4 text-sm text-card-foreground ring-1 ring-foreground/10 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn(
"font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
className
)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-4 group-data-[size=sm]/card:px-3", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn(
"flex items-center rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/card:p-3",
className
)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}
+238
View File
@@ -0,0 +1,238 @@
"use client"
import { useMemo } from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
import { Separator } from "@/components/ui/separator"
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
return (
<fieldset
data-slot="field-set"
className={cn(
"flex flex-col gap-4 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
className
)}
{...props}
/>
)
}
function FieldLegend({
className,
variant = "legend",
...props
}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
return (
<legend
data-slot="field-legend"
data-variant={variant}
className={cn(
"mb-1.5 font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base",
className
)}
{...props}
/>
)
}
function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="field-group"
className={cn(
"group/field-group @container/field-group flex w-full flex-col gap-5 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4",
className
)}
{...props}
/>
)
}
const fieldVariants = cva(
"group/field flex w-full gap-2 data-[invalid=true]:text-destructive",
{
variants: {
orientation: {
vertical: "flex-col *:w-full [&>.sr-only]:w-auto",
horizontal:
"flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
responsive:
"flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
},
},
defaultVariants: {
orientation: "vertical",
},
}
)
function Field({
className,
orientation = "vertical",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
return (
<div
role="group"
data-slot="field"
data-orientation={orientation}
className={cn(fieldVariants({ orientation }), className)}
{...props}
/>
)
}
function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="field-content"
className={cn(
"group/field-content flex flex-1 flex-col gap-0.5 leading-snug",
className
)}
{...props}
/>
)
}
function FieldLabel({
className,
...props
}: React.ComponentProps<typeof Label>) {
return (
<Label
data-slot="field-label"
className={cn(
"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 has-data-checked:border-primary/30 has-data-checked:bg-primary/5 has-[>[data-slot=field]]:rounded-lg has-[>[data-slot=field]]:border *:data-[slot=field]:p-2.5 dark:has-data-checked:border-primary/20 dark:has-data-checked:bg-primary/10",
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col",
className
)}
{...props}
/>
)
}
function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="field-label"
className={cn(
"flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50",
className
)}
{...props}
/>
)
}
function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
return (
<p
data-slot="field-description"
className={cn(
"text-left text-sm leading-normal font-normal text-muted-foreground group-has-data-horizontal/field:text-balance [[data-variant=legend]+&]:-mt-1.5",
"last:mt-0 nth-last-2:-mt-1",
"[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
className
)}
{...props}
/>
)
}
function FieldSeparator({
children,
className,
...props
}: React.ComponentProps<"div"> & {
children?: React.ReactNode
}) {
return (
<div
data-slot="field-separator"
data-content={!!children}
className={cn(
"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
className
)}
{...props}
>
<Separator className="absolute inset-0 top-1/2" />
{children && (
<span
className="relative mx-auto block w-fit bg-background px-2 text-muted-foreground"
data-slot="field-separator-content"
>
{children}
</span>
)}
</div>
)
}
function FieldError({
className,
children,
errors,
...props
}: React.ComponentProps<"div"> & {
errors?: Array<{ message?: string } | undefined>
}) {
const content = useMemo(() => {
if (children) {
return children
}
if (!errors?.length) {
return null
}
const uniqueErrors = [
...new Map(errors.map((error) => [error?.message, error])).values(),
]
if (uniqueErrors?.length == 1) {
return uniqueErrors[0]?.message
}
return (
<ul className="ml-4 flex list-disc flex-col gap-1">
{uniqueErrors.map(
(error, index) =>
error?.message && <li key={index}>{error.message}</li>
)}
</ul>
)
}, [children, errors])
if (!content) {
return null
}
return (
<div
role="alert"
data-slot="field-error"
className={cn("text-sm font-normal text-destructive", className)}
{...props}
>
{content}
</div>
)
}
export {
Field,
FieldLabel,
FieldDescription,
FieldError,
FieldGroup,
FieldLegend,
FieldSeparator,
FieldSet,
FieldContent,
FieldTitle,
}
+156
View File
@@ -0,0 +1,156 @@
"use client"
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="input-group"
role="group"
className={cn(
"group/input-group relative flex h-8 w-full min-w-0 items-center rounded-lg border border-input transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5",
className
)}
{...props}
/>
)
}
const inputGroupAddonVariants = cva(
"flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
{
variants: {
align: {
"inline-start":
"order-first pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem]",
"inline-end":
"order-last pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem]",
"block-start":
"order-first w-full justify-start px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2",
"block-end":
"order-last w-full justify-start px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2",
},
},
defaultVariants: {
align: "inline-start",
},
}
)
function InputGroupAddon({
className,
align = "inline-start",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
return (
<div
role="group"
data-slot="input-group-addon"
data-align={align}
className={cn(inputGroupAddonVariants({ align }), className)}
onClick={(e) => {
if ((e.target as HTMLElement).closest("button")) {
return
}
e.currentTarget.parentElement?.querySelector("input")?.focus()
}}
{...props}
/>
)
}
const inputGroupButtonVariants = cva(
"flex items-center gap-2 text-sm shadow-none",
{
variants: {
size: {
xs: "h-6 gap-1 rounded-[calc(var(--radius)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5",
sm: "",
"icon-xs":
"size-6 rounded-[calc(var(--radius)-3px)] p-0 has-[>svg]:p-0",
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
},
},
defaultVariants: {
size: "xs",
},
}
)
function InputGroupButton({
className,
type = "button",
variant = "ghost",
size = "xs",
...props
}: Omit<React.ComponentProps<typeof Button>, "size"> &
VariantProps<typeof inputGroupButtonVariants>) {
return (
<Button
type={type}
data-size={size}
variant={variant}
className={cn(inputGroupButtonVariants({ size }), className)}
{...props}
/>
)
}
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
className={cn(
"flex items-center gap-2 text-sm text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function InputGroupInput({
className,
...props
}: React.ComponentProps<"input">) {
return (
<Input
data-slot="input-group-control"
className={cn(
"flex-1 rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent",
className
)}
{...props}
/>
)
}
function InputGroupTextarea({
className,
...props
}: React.ComponentProps<"textarea">) {
return (
<Textarea
data-slot="input-group-control"
className={cn(
"flex-1 resize-none rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent",
className
)}
{...props}
/>
)
}
export {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupText,
InputGroupInput,
InputGroupTextarea,
}
+19
View File
@@ -0,0 +1,19 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
className
)}
{...props}
/>
)
}
export { Input }
+24
View File
@@ -0,0 +1,24 @@
"use client"
import * as React from "react"
import { Label as LabelPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
)
}
export { Label }
+28
View File
@@ -0,0 +1,28 @@
"use client"
import * as React from "react"
import { Separator as SeparatorPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot="separator"
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
className
)}
{...props}
/>
)
}
export { Separator }
+18
View File
@@ -0,0 +1,18 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
className
)}
{...props}
/>
)
}
export { Textarea }