A bare minimal stream.place example showing you how to show a stream live on your website
minimal-stream-place.html
1<!doctype html>
2<html lang="en">
3 <head>
4 <meta charset="UTF-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6 <title>Stream.place example</title>
7 </head>
8 <body>
9 <video id="video" autoplay playsinline muted></video>
10
11 <script>
12 // Your atmosphere handle
13 const handle = "pokemon.evil.gay";
14 let pc = null;
15 const video = document.getElementById("video");
16
17 async function connect() {
18 if (pc) disconnect();
19
20 const whepUrl = `https://stream.place/api/playback/${encodeURIComponent(handle)}/webrtc?rendition=source`;
21
22 console.log("Connecting\u2026");
23 console.log(`WHEP endpoint: ${whepUrl}`);
24
25 try {
26 pc = new RTCPeerConnection({
27 iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
28 bundlePolicy: "max-bundle",
29 });
30
31 pc.addTransceiver("video", { direction: "recvonly" });
32 pc.addTransceiver("audio", { direction: "recvonly" });
33
34 pc.ontrack = (event) => {
35 console.log(
36 `Track received: ${event.track.kind}`,
37 "success",
38 );
39 if (event.streams && event.streams[0]) {
40 video.srcObject = event.streams[0];
41 } else {
42 if (!video.srcObject) {
43 video.srcObject = new MediaStream();
44 }
45 video.srcObject.addTrack(event.track);
46 }
47 video.play().catch(() => {});
48 };
49
50 pc.oniceconnectionstatechange = () => {
51 console.log(`ICE: ${pc.iceConnectionState}`);
52 if (
53 pc.iceConnectionState === "connected" ||
54 pc.iceConnectionState === "completed"
55 ) {
56 } else if (
57 pc.iceConnectionState === "failed" ||
58 pc.iceConnectionState === "disconnected"
59 ) {
60 console.log("Connection lost", "error");
61 }
62 };
63
64 pc.onconnectionstatechange = () => {
65 console.log(`Connection: ${pc.connectionState}`);
66 if (pc.connectionState === "failed") {
67 console.log("PeerConnection failed", "error");
68 }
69 };
70
71 const offer = await pc.createOffer();
72 await pc.setLocalDescription(offer);
73 await waitForIceGathering(pc, 2000);
74
75 console.log("Sending SDP offer\u2026");
76
77 const resp = await fetch(whepUrl, {
78 method: "POST",
79 headers: { "Content-Type": "application/sdp" },
80 body: pc.localDescription.sdp,
81 });
82
83 if (!resp.ok) {
84 const errText = await resp.text();
85 throw new Error(`WHEP ${resp.status}: ${errText}`);
86 }
87
88 const answerSdp = await resp.text();
89 console.log("Received SDP answer", "success");
90
91 await pc.setRemoteDescription({
92 type: "answer",
93 sdp: answerSdp,
94 });
95 console.log(
96 "Remote description set, waiting for media\u2026",
97 );
98 } catch (err) {
99 console.log(`Error: ${err.message}`, "error");
100 setStatus("Error", "error");
101 console.error(err);
102 }
103 }
104
105 function disconnect() {
106 if (pc) {
107 pc.close();
108 pc = null;
109 }
110 video.srcObject = null;
111
112 console.log("Disconnected");
113 }
114
115 function waitForIceGathering(peerConnection, timeout) {
116 return new Promise((resolve) => {
117 if (peerConnection.iceGatheringState === "complete") {
118 resolve();
119 return;
120 }
121 const timer = setTimeout(() => {
122 console.log(
123 "ICE gathering timed out, proceeding with candidates",
124 );
125 resolve();
126 }, timeout);
127
128 peerConnection.onicegatheringstatechange = () => {
129 if (peerConnection.iceGatheringState === "complete") {
130 clearTimeout(timer);
131 console.log("ICE gathering complete");
132 resolve();
133 }
134 };
135 });
136 }
137
138 connect();
139 </script>
140 </body>
141</html>