Add Webapp
This commit is contained in:
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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
}
|
||||
</>
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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't have an account? <a href="/signup">Sign up</a>
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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'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>
|
||||
)
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
Reference in new issue
Block a user