minimal streamplace frontend
1function waitForIceGathering(pc: RTCPeerConnection, timeout: number): Promise<void> {
2 return new Promise((resolve) => {
3 if (pc.iceGatheringState === "complete") {
4 resolve();
5 return;
6 }
7 const timer = setTimeout(() => resolve(), timeout);
8 pc.onicegatheringstatechange = () => {
9 if (pc.iceGatheringState === "complete") {
10 clearTimeout(timer);
11 resolve();
12 }
13 };
14 });
15}
16
17export interface WhepConnection {
18 pc: RTCPeerConnection;
19 stream: MediaStream;
20}
21
22export async function connectWhep(handle: string): Promise<WhepConnection> {
23 const whepUrl = `https://stream.place/api/playback/${encodeURIComponent(handle)}/webrtc?rendition=source`;
24
25 const pc = new RTCPeerConnection({
26 iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
27 bundlePolicy: "max-bundle",
28 });
29
30 const stream = new MediaStream();
31
32 pc.addTransceiver("video", { direction: "recvonly" });
33 pc.addTransceiver("audio", { direction: "recvonly" });
34
35 pc.ontrack = (event) => {
36 if (event.streams && event.streams[0]) {
37 for (const track of event.streams[0].getTracks()) {
38 stream.addTrack(track);
39 }
40 } else {
41 stream.addTrack(event.track);
42 }
43 };
44
45 const offer = await pc.createOffer();
46 await pc.setLocalDescription(offer);
47 await waitForIceGathering(pc, 500);
48
49 const resp = await fetch(whepUrl, {
50 method: "POST",
51 headers: { "Content-Type": "application/sdp" },
52 body: pc.localDescription!.sdp,
53 });
54
55 if (!resp.ok) {
56 pc.close();
57 const errText = await resp.text();
58 throw new Error(`WHEP ${resp.status}: ${errText}`);
59 }
60
61 const answerSdp = await resp.text();
62 await pc.setRemoteDescription({ type: "answer", sdp: answerSdp });
63
64 return { pc, stream };
65}