Hi I was playing the calls service of CF, copy pasted the following code directly as advertised by CF:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- This adapter normalizes cross-browser differences in WebRTC APIs. Currently necessary in order to support Firefox. -->
<script
src="https://cdnjs.cloudflare.com/ajax/libs/webrtc-adapter/8.1.2/adapter.min.js"
integrity="sha512-l40eBFtXx+ve5RryIELC3y6/OM6Nu89mLGQd7fg1C93tN6XrkC3supb+/YiD/Y+B8P37kdJjtG1MT1kOO2VzxA=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
></script>
</head>
<body>
<div class="grid">
<h1>Calls Echo Demo</h1>
<div>
<h2>Local stream</h2>
<video id="local-video" autoplay controls muted></video>
</div>
<div>
<h2>Remote echo stream</h2>
<video id="remote-video" autoplay controls></video>
</div>
</div>
<script type="module">
// This is a class the defines the Calls API interactions.
// It's not an SDK but a example of how Calls API can be used.
// This is the App Id provided by the dashboard that identifies this Calls Application.
const appId = "xxx";
// DO NOT USE YOUR SECRET IN THE BROWSER FOR PRODUCTION. It should be kept and used server-side.
const appSecret =
"yyy";
class CallsApp {
constructor(appId, basePath = "https://rtc.live.cloudflare.com/v1") {
this.prefixPath = `${basePath}/apps/${appId}`;
}
async sendRequest(url, body, method = "POST") {
const request = {
method: method,
mode: "cors",
headers: {
"content-type": "application/json",
Authorization: `Bearer ${appSecret}`,
},
body: JSON.stringify(body),
};
const response = await fetch(url, request);
const result = await response.json();
return result;
}
checkErrors(result, tracksCount = 0) {
if (result.errorCode) {
throw new Error(result.errorDescription);
}
for (let i = 0; i < tracksCount; i++) {
if (result.tracks[i].errorCode) {
throw new Error(
`tracks[${i}]: ${result.tracks[i].errorDescription}`
);
}
}
}
// newSession sends the initial offer and creates a session
async newSession(offerSDP) {
const url = `${this.prefixPath}/sessions/new`;
const body = {
sessionDescription: {
type: "offer",
sdp: offerSDP,
},
};
const result = await this.sendRequest(url, body);
this.checkErrors(result);
this.sessionId = result.sessionId;
return result;
}
// newTracks shares local tracks or gets tracks
async newTracks(trackObjects, offerSDP = null) {
const url = `${this.prefixPath}/sessions/${this.sessionId}/tracks/new`;
const body = {
sessionDescription: {
type: "offer",
sdp: offerSDP,
},
tracks: trackObjects,
};
if (!offerSDP) {
delete body["sessionDescription"];
}
const result = await this.sendRequest(url, body);
this.checkErrors(result, trackObjects.length);
return result;
}
// sendAnswerSDP sends an answer SDP if a renegotiation is required
async sendAnswerSDP(answer) {
const url = `${this.prefixPath}/sessions/${this.sessionId}/renegotiate`;
const body = {
sessionDescription: {
type: "answer",
sdp: answer,
},
};
const result = await this.sendRequest(url, body, "PUT");
this.checkErrors(result);
}
}
// Use Cloudflare's STUN server
self.pc = new RTCPeerConnection({
iceServers: [
{
urls: "stun:stun.cloudflare.com:3478",
},
],
bundlePolicy: "max-bundle",
});
// In order to successfully establish a peer connection, we need at least one track to publish.
// In this case, we create two: video & audio
const localStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true,
});
// Get the local video element in the HTML and set the source to show local stream
const localVideoElement = document.getElementById("local-video");
localVideoElement.srcObject = localStream;
// Add sendonly trancievers to the PeerConnection
self.transceivers = localStream.getTracks().map((track) =>
self.pc.addTransceiver(track, {
direction: "sendonly",
})
);
// Create a instance of CallsApp (defined below). Please note that this is not an official SDK but just a demo showing the HTML API.
self.app = new CallsApp(appId);
// Send the first offer and create a session. The returned sessionId is required to retrieve any track published by this peer
await self.pc.setLocalDescription(await self.pc.createOffer());
const newSessionResult = await self.app.newSession(
self.pc.localDescription.sdp
);
await self.pc.setRemoteDescription(
new RTCSessionDescription(newSessionResult.sessionDescription)
);
// Make the peer connection was established
await new Promise((resolve, reject) => {
self.pc.addEventListener("iceconnectionstatechange", (ev) => {
if (ev.target.iceConnectionState === "connected") {
resolve();
}
setTimeout(reject, 5000, "connect timeout");
});
});
// We associate a trackName to a transceiver identified by a mid (media ID). This way the track
// is remotely reachable by the tuple (sessionId, trackName)
let trackObjects = self.transceivers.map((transceiver) => {
return {
location: "local",
mid: transceiver.mid,
trackName: transceiver.sender.track.id,
};
});
// Get local description, create a new track, set remote description with the response
await self.pc.setLocalDescription(await self.pc.createOffer());
const newLocalTracksResult = await self.app.newTracks(
trackObjects,
self.pc.localDescription.sdp
);
await self.pc.setRemoteDescription(
new RTCSessionDescription(newLocalTracksResult.sessionDescription)
);
// At this point in code, we are successfully sending local stream to Cloudflare Calls.
// Now, we will pull the same stream from Cloudflare Calls.
// Update trackObjects to reference the tracks as "remote"
trackObjects = trackObjects.map((trackObject) => {
return {
location: "remote",
sessionId: self.app.sessionId,
trackName: trackObject.trackName,
};
});
// Prepare to receive the tracks before asking for them
const remoteTracksPromise = new Promise((resolve) => {
let tracks = [];
self.pc.ontrack = (event) => {
tracks.push(event.track);
console.debug(`Got track mid=${event.track.mid}`);
if (tracks.length >= 2) {
// remote video & audio are ready
resolve(tracks);
}
};
});
// Calls API request to ask for the tracks
const newRemoteTracksResult = await self.app.newTracks(trackObjects);
if (newRemoteTracksResult.requiresImmediateRenegotiation) {
switch (newRemoteTracksResult.sessionDescription.type) {
case "offer":
// We let Cloudflare know we're ready to receive the tracks
await self.pc.setRemoteDescription(
new RTCSessionDescription(
newRemoteTracksResult.sessionDescription
)
);
await self.pc.setLocalDescription(await self.pc.createAnswer());
await self.app.sendAnswerSDP(self.pc.localDescription.sdp);
break;
case "answer":
throw new Error("An offer SDP was expected");
}
}
// Once started receiving the tracks (video & audio) send the data to the video tag
const remoteTracks = await remoteTracksPromise;
const remoteVideoElement = document.getElementById("remote-video");
const remoteStream = new MediaStream();
remoteStream.addTrack(remoteTracks[0]);
remoteStream.addTrack(remoteTracks[1]);
remoteVideoElement.srcObject = remoteStream;
</script>
<style>
/* Styles are safe to ignore, just here for demo */
html {
color-scheme: light dark;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue",
sans-serif;
background: white;
color: black;
}
body,
h1,
h2 {
margin: 0;
}
h1,
h2 {
font-weight: 400;
}
h1 {
font-size: 1.5rem;
grid-column: 1 / -1;
}
h2 {
font-size: 1rem;
margin-bottom: 0.5rem;
}
video {
width: 100%;
}
.grid {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 1rem;
}
@media (max-width: 500px) {
.grid {
grid-template-columns: minmax(0, 1fr);
}
}
</style>
</body>
</html>
This wasn't working so I checked the demo-app, which also isn't working, the demo-app produces the following error log:
Error: not_found_track_error: Track not found on remote peer
at I.pullTrack (_room-UE3GO4BA.js:35:9039)
Hi I was playing the calls service of CF, copy pasted the following code directly as advertised by CF:
This wasn't working so I checked the demo-app, which also isn't working, the demo-app produces the following error log: