streamverse
Version:
Zero-config real-time video calls and streaming. WebRTC made simple for developers.
229 lines • 7.38 kB
JavaScript
// 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();
}
};
export {
createStreamShareClient
};
//# sourceMappingURL=index.js.map