Created
January 15, 2023 14:30
-
-
Save guiseek/8d98590573a6db034e7f6084b444f7ca to your computer and use it in GitHub Desktop.
Chat WebRTC DataChannel com Broadcast Channel
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8" /> | |
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
<title>Vite + TS</title> | |
</head> | |
<body> | |
<template id="messageItemTemplate"> | |
<dt></dt> | |
<dd></dd> | |
</template> | |
<div id="sendReceive"> | |
<div id="receive"> | |
<h2>Mensagens</h2> | |
<dl></dl> | |
</div> | |
<div id="send"> | |
<h2>Envio</h2> | |
<form id="chatForm" novalidate onreset="sessionStorage.removeItem('nickname')"> | |
<section> | |
<label for="nickname">Apelido</label> | |
<input | |
autofocus | |
type="text" | |
id="nickname" | |
name="nickname" | |
onfocus="turnReadOnly(this)" | |
autocomplete="nickname" | |
placeholder="guiseek" | |
required | |
/> | |
</section> | |
<textarea | |
id="dataChannelSend" | |
disabled | |
name="message" | |
placeholder="Clique em Iniciar, coloque seu nome, escreva uma mensagem e então clique em Enviar." | |
rows="3" | |
cols="30" | |
required | |
></textarea> | |
<output></output> | |
</form> | |
</div> | |
</div> | |
<div id="buttons"> | |
<button type="button" form="chatForm" id="startButton" disabled>Iniciar</button> | |
<button id="sendButton" type="submit" form="chatForm" disabled> | |
Enviar | |
</button> | |
<button type="button" form="chatForm" id="closeButton" disabled>Fechar</button> | |
<button type="reset" form="chatForm">Limpar</button> | |
</div> | |
<script type="module" src="/src/main.ts"></script> | |
<script> | |
function turnReadOnly(el) { | |
const nickname = sessionStorage.getItem('nickname') | |
if (nickname) { | |
el.value = nickname | |
} | |
let hasValue = !!el.value && el.value.length > 0 | |
let isActive = document.activeElement.isSameNode(el) | |
if (hasValue && isActive) el.readOnly = true | |
} | |
</script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
type ChatForm = { | |
nickname: string | |
message: string | |
} | |
const startButton = document.querySelector('#startButton') as HTMLButtonElement | |
const closeButton = document.querySelector('#closeButton') as HTMLButtonElement | |
const sendButton = document.querySelector('#sendButton') as HTMLButtonElement | |
const messageItemTemplate = document.querySelector( | |
'#messageItemTemplate' | |
) as HTMLTemplateElement | |
const dataChannelSend = document.querySelector( | |
'textarea#dataChannelSend' | |
) as HTMLTextAreaElement | |
const chatList = document.querySelector('dl') as HTMLDListElement | |
const chatForm = document.querySelector('form') as HTMLFormElement | |
const chatInput = chatForm.querySelector('input') as HTMLInputElement | |
const chatOutput = chatForm.querySelector('output') as HTMLOutputElement | |
chatForm.onsubmit = sendData | |
let lastCode = '' | |
dataChannelSend.onkeydown = (ev) => { | |
if (ev.code !== 'Enter') { | |
lastCode = ev.code | |
} | |
} | |
dataChannelSend.onkeyup = (ev) => { | |
if (lastCode !== 'ShiftLeft' && ev.code === 'Enter') { | |
// chatForm.submit() | |
console.log(lastCode, ev.code) | |
ev.preventDefault() | |
ev.stopPropagation() | |
console.log(ev) | |
sendData(ev) | |
} | |
} | |
// chatForm.onreset = resetNickname | |
const getNickname = () => { | |
return sessionStorage.getItem('nickname') | |
} | |
// function resetNickname() { | |
// return sessionStorage.removeItem('nickname') | |
// } | |
const setNickname = (value: string) => { | |
if (value !== getNickname()) { | |
sessionStorage.setItem('nickname', value) | |
} | |
} | |
onload = () => { | |
const nickname = getNickname() | |
if (nickname) { | |
setNickname(nickname) | |
chatInput.value = nickname | |
// chatInput.readOnly = true | |
} | |
} | |
let pc: RTCPeerConnection | |
let sendChannel: RTCDataChannel | |
let receiveChannel: RTCDataChannel | |
const signaling = new BroadcastChannel('webrtc') | |
signaling.onmessage = (e) => { | |
switch (e.data.type) { | |
case 'offer': | |
if (!pc) { | |
handleOffer(e.data) | |
} | |
break | |
case 'answer': | |
if (pc) { | |
handleAnswer(e.data) | |
} | |
break | |
case 'candidate': | |
handleCandidate(e.data) | |
break | |
case 'ready': | |
// A second tab joined. This tab will enable the start button unless in a call already. | |
if (pc) { | |
console.log('already in call, ignoring') | |
return | |
} | |
startButton.disabled = false | |
break | |
case 'bye': | |
if (pc) { | |
hangup() | |
} | |
break | |
default: | |
console.log('unhandled', e) | |
break | |
} | |
} | |
signaling.postMessage({type: 'ready'}) | |
startButton.onclick = async () => { | |
startButton.disabled = true | |
closeButton.disabled = false | |
await createPeerConnection() | |
sendChannel = pc.createDataChannel('sendDataChannel') | |
sendChannel.onopen = onSendChannelStateChange | |
sendChannel.onmessage = onSendChannelMessageCallback | |
sendChannel.onclose = onSendChannelStateChange | |
const offer = await pc.createOffer() | |
// debugger | |
signaling.postMessage({type: 'offer', sdp: offer.sdp}) | |
await pc.setLocalDescription(offer) | |
} | |
closeButton.onclick = async () => { | |
hangup() | |
signaling.postMessage({type: 'bye'}) | |
} | |
const hangup = async () => { | |
if (pc) { | |
pc.close() | |
} | |
if (!dataChannelSend) { | |
return | |
} | |
chatOutput.textContent = 'Closed peer connections' | |
startButton.disabled = false | |
sendButton.disabled = true | |
closeButton.disabled = true | |
dataChannelSend.value = '' | |
dataChannelSend.disabled = true | |
} | |
const createPeerConnection = async () => { | |
pc = new RTCPeerConnection() | |
pc.onicecandidate = (e) => { | |
const message = { | |
type: 'candidate', | |
candidate: null, | |
} as Record<string, string | number | null> | |
if (e.candidate) { | |
message.candidate = e.candidate.candidate | |
message.sdpMid = e.candidate.sdpMid | |
message.sdpMLineIndex = e.candidate.sdpMLineIndex | |
} | |
signaling.postMessage(message) | |
} | |
} | |
const handleOffer = async (offer: RTCSessionDescriptionInit) => { | |
await createPeerConnection() | |
pc.ondatachannel = receiveChannelCallback | |
await pc.setRemoteDescription(offer) | |
const answer = await pc.createAnswer() | |
signaling.postMessage({type: 'answer', sdp: answer.sdp}) | |
await pc.setLocalDescription(answer) | |
} | |
async function handleAnswer(answer: RTCSessionDescriptionInit) { | |
await pc.setRemoteDescription(answer) | |
} | |
async function handleCandidate( | |
candidate: RTCPeerConnectionIceEvent | RTCIceCandidate | |
) { | |
if (!pc) { | |
console.error('no peerconnection') | |
return | |
} | |
if (!candidate.candidate) { | |
await pc.addIceCandidate(null as unknown as RTCIceCandidate) | |
} else { | |
await pc.addIceCandidate(candidate as RTCIceCandidate) | |
} | |
} | |
function parseFormData<R>(form: HTMLFormElement) { | |
return Object.fromEntries(new FormData(form).entries()) as R | |
} | |
const addChatItem = (message: string, from?: string) => { | |
const template = messageItemTemplate.content.cloneNode(true) as HTMLElement | |
const dt = template.querySelector('dt') | |
const dd = template.querySelector('dd') | |
if (dt && dd) { | |
dt.textContent = `${from}` | |
dd.textContent = message | |
} | |
chatList.appendChild(template) | |
} | |
function sendData<E extends Event>(ev: E) { | |
ev.preventDefault() | |
ev.stopPropagation() | |
const data = parseFormData<ChatForm>(chatForm) | |
if (sendChannel) { | |
sendChannel.send(JSON.stringify(data)) | |
} else { | |
receiveChannel.send(JSON.stringify(data)) | |
} | |
const {message, nickname} = data | |
setNickname(nickname) | |
if (!chatInput.readOnly) { | |
chatInput.readOnly = true | |
} | |
addChatItem(message.toString(), nickname.toString()) | |
dataChannelSend.value = '' | |
} | |
function receiveChannelCallback(event: RTCDataChannelEvent) { | |
receiveChannel = event.channel | |
receiveChannel.onmessage = onReceiveChannelMessageCallback | |
receiveChannel.onopen = onReceiveChannelStateChange | |
receiveChannel.onclose = onReceiveChannelStateChange | |
} | |
function onReceiveChannelMessageCallback(event: MessageEvent) { | |
chatOutput.textContent = 'Received Message' | |
const {message, nickname} = JSON.parse(event.data) ?? {} | |
addChatItem(message, nickname) | |
} | |
function onSendChannelMessageCallback(event: MessageEvent) { | |
chatOutput.textContent = 'Received Message' | |
const {message, nickname} = JSON.parse(event.data) ?? {} | |
addChatItem(message, nickname) | |
} | |
function onSendChannelStateChange() { | |
const {readyState} = sendChannel | |
chatOutput.textContent = `O estado do canal de envio é: ${readyState}` | |
if (readyState === 'open') { | |
dataChannelSend.disabled = false | |
dataChannelSend.focus() | |
sendButton.disabled = false | |
closeButton.disabled = false | |
} else { | |
dataChannelSend.disabled = true | |
sendButton.disabled = true | |
closeButton.disabled = true | |
} | |
} | |
function onReceiveChannelStateChange() { | |
const {readyState} = receiveChannel | |
chatOutput.textContent = `O estado do canal de recepção é: ${readyState}` | |
if (readyState === 'open') { | |
dataChannelSend.disabled = false | |
sendButton.disabled = false | |
closeButton.disabled = false | |
} else { | |
dataChannelSend.disabled = true | |
sendButton.disabled = true | |
closeButton.disabled = true | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
:root { | |
--font-family: Inter, Avenir, Helvetica, Arial, sans-serif; | |
// color-scheme: light dark; | |
font-synthesis: none; | |
text-rendering: optimizeLegibility; | |
-webkit-font-smoothing: antialiased; | |
-moz-osx-font-smoothing: grayscale; | |
-webkit-text-size-adjust: 100%; | |
} | |
a { | |
font-weight: 500; | |
color: #646cff; | |
text-decoration: inherit; | |
} | |
a:hover { | |
color: #535bf2; | |
} | |
body { | |
margin: 0; | |
display: flex; | |
flex-direction: column; | |
place-items: center; | |
min-width: 320px; | |
min-height: 100vh; | |
font-family: var(--font-family); | |
font-size: 16px; | |
line-height: 24px; | |
font-weight: 400; | |
} | |
h1 { | |
font-size: 3.2em; | |
line-height: 1.1; | |
} | |
#sendReceive { | |
display: flex; | |
flex-direction: column; | |
column-gap: 36px; | |
} | |
dl, | |
form, | |
section { | |
display: flex; | |
flex-direction: column; | |
} | |
textarea { | |
font-family: var(--font-family); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment