Skip to content

Instantly share code, notes, and snippets.

@guiseek
Created July 12, 2023 06:45
Show Gist options
  • Save guiseek/ca6d240f53404d135139861fdec35149 to your computer and use it in GitHub Desktop.
Save guiseek/ca6d240f53404d135139861fdec35149 to your computer and use it in GitHub Desktop.
WebRTC Local
function create<K extends keyof HTMLElementTagNameMap>(
name: K,
attributes: Partial<HTMLElementTagNameMap[K]>,
...children: Element[]
): HTMLElementTagNameMap[K] {
const el = document.createElement(name)
if (children) el.append(...children)
return Object.assign(el, attributes)
}
interface Message {
id: string
type: 'offer' | 'answer' | 'candidate'
sdp?: RTCSessionDescriptionInit
candidate?: RTCIceCandidateInit
}
interface CallbackFunction<T> {
(value: T): void
}
class Signaling {
id = crypto.randomUUID()
#onOffer: CallbackFunction<Message>[] = []
set onOffer(cb: CallbackFunction<Message>) {
this.#onOffer.push(cb)
}
#onAnswer: CallbackFunction<Message>[] = []
set onAnswer(cb: CallbackFunction<Message>) {
this.#onAnswer.push(cb)
}
#onCandidate: CallbackFunction<Message>[] = []
set onCandidate(cb: CallbackFunction<Message>) {
this.#onCandidate.push(cb)
}
#provider
constructor(name: string) {
this.#provider = new BroadcastChannel(name)
this.#provider.onmessage = async ({data}: MessageEvent<Message>) => {
if (data.type === 'answer') for (const cb of this.#onAnswer) cb(data)
if (data.type === 'offer') for (const cb of this.#onOffer) cb(data)
if (data.type === 'candidate')
for (const cb of this.#onCandidate) cb(data)
}
}
sendOffer(sdp: RTCSessionDescription) {
this.#provider.postMessage({id: this.id, type: 'offer', sdp: sdp.toJSON()})
}
sendAnswer(sdp: RTCSessionDescription) {
this.#provider.postMessage({id: this.id, type: 'answer', sdp: sdp.toJSON()})
}
sendCandidate({candidate}: RTCIceCandidateInit) {
this.#provider.postMessage({type: 'candidate', id: this.id, candidate})
}
}
const signaling = new Signaling('webrtc')
const peer = new RTCPeerConnection()
peer.onicecandidate = ({candidate}) => {
if (candidate) signaling.sendCandidate(new RTCIceCandidate(candidate))
}
signaling.onAnswer = (answer) => {
if (answer.id !== signaling.id && answer.sdp) {
console.log(answer)
if (peer.signalingState !== 'stable') {
peer.setRemoteDescription(answer.sdp)
}
}
}
signaling.onOffer = async (offer) => {
if (offer.id !== signaling.id && offer.sdp) {
if (peer.signalingState !== 'stable') {
peer.setRemoteDescription(offer.sdp)
peer.createAnswer().then(async (sdp) => {
await peer.setLocalDescription(sdp)
signaling.sendAnswer(new RTCSessionDescription(sdp))
})
}
}
}
const channel = peer.createDataChannel('channel')
channel.onmessage = ({data}) => {
console.log(`%c ${data}`, 'font-size: 24px')
}
channel.onopen = console.log
peer.oniceconnectionstatechange = console.log
peer.onconnectionstatechange = console.log
peer.onnegotiationneeded = (ev) => {
console.log(ev)
if (peer.signalingState !== 'have-remote-offer') {
peer.createOffer().then(async (sdp) => {
await peer.setLocalDescription(sdp)
signaling.sendOffer(new RTCSessionDescription(sdp))
})
}
}
navigator.mediaDevices.getUserMedia({audio: {
echoCancellation: true
}}).then((stream) => {
const local = create('audio', {
srcObject: stream,
controls: true,
autoplay: true,
muted: true,
})
document.body.appendChild(local)
stream.getTracks().forEach((track) => {
peer.addTrack(track, stream)
})
})
peer.ontrack = ({track, streams}) => {
console.log(track, streams)
const remote = create('audio', {
srcObject: streams[0],
controls: true,
autoplay: true,
})
document.body.appendChild(remote)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment