Skip to content

Instantly share code, notes, and snippets.

@guiseek
Created January 15, 2023 14:30
Show Gist options
  • Save guiseek/8d98590573a6db034e7f6084b444f7ca to your computer and use it in GitHub Desktop.
Save guiseek/8d98590573a6db034e7f6084b444f7ca to your computer and use it in GitHub Desktop.
Chat WebRTC DataChannel com Broadcast Channel
<!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>
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
}
}
: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