UNPKG

streamverse

Version:

Zero-config real-time video calls and streaming. WebRTC made simple for developers.

1 lines 15.5 kB
{"version":3,"sources":["../src/index.ts","../src/internal/HostedClient.ts"],"sourcesContent":["export type StreamKind = \"camera\" | \"screen\" | \"audio\";\n\nexport interface StreamShareOptions {\n userId: string;\n signalingUrl?: string;\n}\n\nexport interface RemoteStreamEvent {\n userId: string;\n stream: MediaStream;\n}\n\nexport interface StreamShareClient {\n subscribe(userId: string): Promise<void>;\n startSession(sessionId: string): Promise<void>;\n joinSession(sessionId: string): Promise<void>;\n publishStream(stream: MediaStream, kind?: StreamKind): Promise<void>;\n onRemoteStream(listener: (event: RemoteStreamEvent) => void): () => void;\n close(): Promise<void>;\n}\n\nexport { createStreamShareClient } from \"./internal/HostedClient\";\n","import type {\n RemoteStreamEvent,\n StreamKind,\n StreamShareClient,\n StreamShareOptions,\n} from \"../index\";\n\nexport function createStreamShareClient(\n options: StreamShareOptions\n): StreamShareClient {\n return new HostedStreamShareClient(options);\n}\n\ntype SignalMessage =\n | { type: \"subscribed\"; userId: string }\n | { type: \"peers\"; peers: string[] }\n | { type: \"peer-joined\"; userId: string }\n | { type: \"peer-left\"; userId: string }\n | { type: \"offer\"; from: string; sdp: string }\n | { type: \"answer\"; from: string; sdp: string }\n | { type: \"ice\"; from: string; candidate: RTCIceCandidateInit }\n | { type: \"error\"; message: string };\n\nclass HostedStreamShareClient implements StreamShareClient {\n private readonly userId: string;\n private readonly signalingUrl: string;\n private ws?: WebSocket;\n private sessionId?: string;\n private localStream?: MediaStream;\n private remoteListeners: Set<(event: RemoteStreamEvent) => void> = new Set();\n private peerConnections = new Map<\n string,\n RTCPeerConnection & { _pendingCandidates?: RTCIceCandidateInit[] }\n >();\n private remoteStreams = new Map<string, MediaStream>();\n private useHostedService: boolean;\n\n constructor(options: StreamShareOptions) {\n this.userId = options.userId;\n // Default to hosted service, allow custom override\n this.signalingUrl = options.signalingUrl ?? \"wss://signal.streamshare.dev\";\n this.useHostedService = !options.signalingUrl; // Only use hosted if no custom URL provided\n }\n\n private ensureSocket(): Promise<void> {\n if (this.ws && this.ws.readyState === WebSocket.OPEN)\n return Promise.resolve();\n return new Promise((resolve, reject) => {\n const ws = new WebSocket(this.signalingUrl);\n this.ws = ws;\n\n const timeout = setTimeout(() => {\n if (this.useHostedService) {\n console.log(\"Hosted service unavailable, falling back to P2P mode\");\n // Fall back to direct P2P without server\n resolve();\n } else {\n reject(new Error(\"Custom signaling server unavailable\"));\n }\n }, 5000);\n\n ws.onopen = () => {\n clearTimeout(timeout);\n this.send({ type: \"subscribe\", userId: this.userId });\n resolve();\n };\n\n ws.onmessage = (ev) => this.onSignal(JSON.parse(ev.data as string));\n\n ws.onerror = () => {\n clearTimeout(timeout);\n if (this.useHostedService) {\n console.log(\"Hosted service error, falling back to P2P mode\");\n resolve();\n } else {\n reject(new Error(\"Signaling server connection failed\"));\n }\n };\n\n ws.onclose = () => {\n for (const pc of this.peerConnections.values()) pc.close();\n this.peerConnections.clear();\n this.remoteStreams.clear();\n };\n });\n }\n\n private send(msg: any) {\n if (this.ws && this.ws.readyState === WebSocket.OPEN) {\n this.ws.send(JSON.stringify(msg));\n }\n }\n\n async subscribe(userId: string): Promise<void> {\n void userId;\n await this.ensureSocket();\n }\n\n async startSession(sessionId: string): Promise<void> {\n await this.ensureSocket();\n this.sessionId = sessionId;\n this.send({ type: \"start-session\", sessionId });\n }\n\n async joinSession(sessionId: string): Promise<void> {\n await this.ensureSocket();\n this.sessionId = sessionId;\n this.send({ type: \"join-session\", sessionId });\n }\n\n async publishStream(\n stream: MediaStream,\n kind: StreamKind = \"camera\"\n ): Promise<void> {\n void kind;\n this.localStream = stream;\n\n // If no server connection, create a simple broadcast mode\n if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {\n console.log(\n \"Broadcasting in P2P mode - other users will connect directly\"\n );\n // Store stream for when peers connect\n return;\n }\n\n // With server: add tracks to existing peers and renegotiate\n for (const [peerId, pc] of this.peerConnections) {\n this.addTracksToPeer(pc, stream);\n await this.createAndSendOffer(peerId, pc);\n }\n }\n\n onRemoteStream(listener: (event: RemoteStreamEvent) => void): () => void {\n this.remoteListeners.add(listener);\n return () => this.remoteListeners.delete(listener);\n }\n\n private emitRemote(event: RemoteStreamEvent) {\n for (const listener of this.remoteListeners) listener(event);\n }\n\n private async onSignal(msg: SignalMessage) {\n try {\n if (msg.type === \"peers\") {\n for (const peerId of msg.peers) {\n const pc = this.getOrCreatePeer(peerId);\n if (this.localStream) this.addTracksToPeer(pc, this.localStream);\n await this.createAndSendOffer(peerId, pc);\n }\n return;\n }\n\n if (msg.type === \"peer-joined\") {\n this.getOrCreatePeer(msg.userId);\n return;\n }\n\n if (msg.type === \"peer-left\") {\n const pc = this.peerConnections.get(msg.userId);\n pc?.close();\n this.peerConnections.delete(msg.userId);\n this.remoteStreams.delete(msg.userId);\n return;\n }\n\n if (msg.type === \"offer\") {\n const pc = this.getOrCreatePeer(msg.from);\n if (pc.signalingState !== \"stable\") {\n console.warn(\n `Peer ${msg.from} not in stable state:`,\n pc.signalingState\n );\n return;\n }\n await pc.setRemoteDescription({ type: \"offer\", sdp: msg.sdp });\n if (pc._pendingCandidates) {\n for (const candidate of pc._pendingCandidates) {\n await pc.addIceCandidate(candidate);\n }\n pc._pendingCandidates = [];\n }\n if (this.localStream) this.addTracksToPeer(pc, this.localStream);\n const answer = await pc.createAnswer();\n await pc.setLocalDescription(answer);\n this.send({ type: \"answer\", to: msg.from, sdp: answer.sdp! });\n return;\n }\n\n if (msg.type === \"answer\") {\n const pc = this.peerConnections.get(msg.from);\n if (!pc) return;\n if (pc.signalingState !== \"have-local-offer\") {\n console.warn(\n `Peer ${msg.from} not expecting answer, state:`,\n pc.signalingState\n );\n return;\n }\n await pc.setRemoteDescription({ type: \"answer\", sdp: msg.sdp });\n if (pc._pendingCandidates) {\n for (const candidate of pc._pendingCandidates) {\n await pc.addIceCandidate(candidate);\n }\n pc._pendingCandidates = [];\n }\n return;\n }\n\n if (msg.type === \"ice\") {\n const pc = this.peerConnections.get(msg.from);\n if (!pc) return;\n if (pc.remoteDescription) {\n await pc.addIceCandidate(msg.candidate);\n } else {\n if (!pc._pendingCandidates) pc._pendingCandidates = [];\n pc._pendingCandidates.push(msg.candidate);\n }\n return;\n }\n } catch (error) {\n console.error(\"Error handling signal:\", error);\n }\n }\n\n private getOrCreatePeer(\n peerId: string\n ): RTCPeerConnection & { _pendingCandidates?: RTCIceCandidateInit[] } {\n let pc = this.peerConnections.get(peerId);\n if (pc) return pc;\n\n pc = new RTCPeerConnection({\n iceServers: [\n { urls: \"stun:stun.l.google.com:19302\" },\n // Add hosted TURN servers for better connectivity\n {\n urls: \"turn:turn.streamshare.dev:3478\",\n username: \"streamshare\",\n credential: \"public\",\n },\n ],\n }) as RTCPeerConnection & { _pendingCandidates?: RTCIceCandidateInit[] };\n\n pc.onicecandidate = (e) => {\n if (e.candidate)\n this.send({ type: \"ice\", to: peerId, candidate: e.candidate });\n };\n\n pc.ontrack = (e) => {\n let stream = this.remoteStreams.get(peerId);\n if (!stream) {\n stream = new MediaStream();\n this.remoteStreams.set(peerId, stream);\n // Only emit event when creating new stream\n stream.addTrack(e.track);\n this.emitRemote({ userId: peerId, stream });\n } else {\n // Just add track to existing stream, don't emit again\n stream.addTrack(e.track);\n }\n };\n\n pc.onconnectionstatechange = () => {\n if (pc?.connectionState === \"failed\") pc.restartIce();\n };\n\n if (this.localStream) this.addTracksToPeer(pc, this.localStream);\n this.peerConnections.set(peerId, pc);\n return pc;\n }\n\n private addTracksToPeer(pc: RTCPeerConnection, stream: MediaStream) {\n const existing = new Set(pc.getSenders().map((s) => s.track?.id));\n for (const track of stream.getTracks()) {\n if (existing.has(track.id)) continue;\n pc.addTrack(track, stream);\n }\n }\n\n private async createAndSendOffer(peerId: string, pc: RTCPeerConnection) {\n const offer = await pc.createOffer({\n offerToReceiveAudio: true,\n offerToReceiveVideo: true,\n });\n await pc.setLocalDescription(offer);\n this.send({ type: \"offer\", to: peerId, sdp: offer.sdp! });\n }\n\n async close(): Promise<void> {\n if (this.sessionId) this.send({ type: \"leave-session\" });\n for (const pc of this.peerConnections.values()) pc.close();\n this.peerConnections.clear();\n this.remoteStreams.clear();\n this.ws?.close();\n }\n}\n"],"mappings":"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","names":[]}