UNPKG

streamverse

Version:

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

256 lines (254 loc) 8.44 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { createStreamShareClient: () => createStreamShareClient }); module.exports = __toCommonJS(index_exports); // src/internal/HostedClient.ts function createStreamShareClient(options) { return new HostedStreamShareClient(options); } var HostedStreamShareClient = class { constructor(options) { this.remoteListeners = /* @__PURE__ */ new Set(); this.peerConnections = /* @__PURE__ */ new Map(); this.remoteStreams = /* @__PURE__ */ new Map(); this.userId = options.userId; this.signalingUrl = options.signalingUrl ?? "wss://signal.streamshare.dev"; this.useHostedService = !options.signalingUrl; } ensureSocket() { if (this.ws && this.ws.readyState === WebSocket.OPEN) return Promise.resolve(); return new Promise((resolve, reject) => { const ws = new WebSocket(this.signalingUrl); this.ws = ws; const timeout = setTimeout(() => { if (this.useHostedService) { console.log("Hosted service unavailable, falling back to P2P mode"); resolve(); } else { reject(new Error("Custom signaling server unavailable")); } }, 5e3); ws.onopen = () => { clearTimeout(timeout); this.send({ type: "subscribe", userId: this.userId }); resolve(); }; ws.onmessage = (ev) => this.onSignal(JSON.parse(ev.data)); ws.onerror = () => { clearTimeout(timeout); if (this.useHostedService) { console.log("Hosted service error, falling back to P2P mode"); resolve(); } else { reject(new Error("Signaling server connection failed")); } }; ws.onclose = () => { for (const pc of this.peerConnections.values()) pc.close(); this.peerConnections.clear(); this.remoteStreams.clear(); }; }); } send(msg) { if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify(msg)); } } async subscribe(userId) { void userId; await this.ensureSocket(); } async startSession(sessionId) { await this.ensureSocket(); this.sessionId = sessionId; this.send({ type: "start-session", sessionId }); } async joinSession(sessionId) { await this.ensureSocket(); this.sessionId = sessionId; this.send({ type: "join-session", sessionId }); } async publishStream(stream, kind = "camera") { void kind; this.localStream = stream; if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { console.log( "Broadcasting in P2P mode - other users will connect directly" ); return; } for (const [peerId, pc] of this.peerConnections) { this.addTracksToPeer(pc, stream); await this.createAndSendOffer(peerId, pc); } } onRemoteStream(listener) { this.remoteListeners.add(listener); return () => this.remoteListeners.delete(listener); } emitRemote(event) { for (const listener of this.remoteListeners) listener(event); } async onSignal(msg) { try { if (msg.type === "peers") { for (const peerId of msg.peers) { const pc = this.getOrCreatePeer(peerId); if (this.localStream) this.addTracksToPeer(pc, this.localStream); await this.createAndSendOffer(peerId, pc); } return; } if (msg.type === "peer-joined") { this.getOrCreatePeer(msg.userId); return; } if (msg.type === "peer-left") { const pc = this.peerConnections.get(msg.userId); pc?.close(); this.peerConnections.delete(msg.userId); this.remoteStreams.delete(msg.userId); return; } if (msg.type === "offer") { const pc = this.getOrCreatePeer(msg.from); if (pc.signalingState !== "stable") { console.warn( `Peer ${msg.from} not in stable state:`, pc.signalingState ); return; } await pc.setRemoteDescription({ type: "offer", sdp: msg.sdp }); if (pc._pendingCandidates) { for (const candidate of pc._pendingCandidates) { await pc.addIceCandidate(candidate); } pc._pendingCandidates = []; } if (this.localStream) this.addTracksToPeer(pc, this.localStream); const answer = await pc.createAnswer(); await pc.setLocalDescription(answer); this.send({ type: "answer", to: msg.from, sdp: answer.sdp }); return; } if (msg.type === "answer") { const pc = this.peerConnections.get(msg.from); if (!pc) return; if (pc.signalingState !== "have-local-offer") { console.warn( `Peer ${msg.from} not expecting answer, state:`, pc.signalingState ); return; } await pc.setRemoteDescription({ type: "answer", sdp: msg.sdp }); if (pc._pendingCandidates) { for (const candidate of pc._pendingCandidates) { await pc.addIceCandidate(candidate); } pc._pendingCandidates = []; } return; } if (msg.type === "ice") { const pc = this.peerConnections.get(msg.from); if (!pc) return; if (pc.remoteDescription) { await pc.addIceCandidate(msg.candidate); } else { if (!pc._pendingCandidates) pc._pendingCandidates = []; pc._pendingCandidates.push(msg.candidate); } return; } } catch (error) { console.error("Error handling signal:", error); } } getOrCreatePeer(peerId) { let pc = this.peerConnections.get(peerId); if (pc) return pc; pc = new RTCPeerConnection({ iceServers: [ { urls: "stun:stun.l.google.com:19302" }, // Add hosted TURN servers for better connectivity { urls: "turn:turn.streamshare.dev:3478", username: "streamshare", credential: "public" } ] }); pc.onicecandidate = (e) => { if (e.candidate) this.send({ type: "ice", to: peerId, candidate: e.candidate }); }; pc.ontrack = (e) => { let stream = this.remoteStreams.get(peerId); if (!stream) { stream = new MediaStream(); this.remoteStreams.set(peerId, stream); stream.addTrack(e.track); this.emitRemote({ userId: peerId, stream }); } else { stream.addTrack(e.track); } }; pc.onconnectionstatechange = () => { if (pc?.connectionState === "failed") pc.restartIce(); }; if (this.localStream) this.addTracksToPeer(pc, this.localStream); this.peerConnections.set(peerId, pc); return pc; } addTracksToPeer(pc, stream) { const existing = new Set(pc.getSenders().map((s) => s.track?.id)); for (const track of stream.getTracks()) { if (existing.has(track.id)) continue; pc.addTrack(track, stream); } } async createAndSendOffer(peerId, pc) { const offer = await pc.createOffer({ offerToReceiveAudio: true, offerToReceiveVideo: true }); await pc.setLocalDescription(offer); this.send({ type: "offer", to: peerId, sdp: offer.sdp }); } async close() { if (this.sessionId) this.send({ type: "leave-session" }); for (const pc of this.peerConnections.values()) pc.close(); this.peerConnections.clear(); this.remoteStreams.clear(); this.ws?.close(); } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { createStreamShareClient }); //# sourceMappingURL=index.cjs.map