Simple Webcast is an open source browser application to broadcast yourself in a simple way. You press Broadcast and receives a link that you share with your viewers. A link they simply open up in their browsers to watch your webcast. A demo is available on webcast.eyevinn.technology and we will in this blog post describe how it works and how you can set it up yourself.
The application uses Eyevinn's open source WHIP NPM libraries @eyevinn/whip-web-client and @eyevinn/whip-endpoint.
WHIP (WebRTC-HTTP Ingestion Protocol) is a new standard for WebRTC based ingestion. This means that the sender part of this application can be used with any type of WHIP compatible media server.
What is WHIP?
WHIP is an initiative to help adopt WebRTC in the broadcasting / streaming industry. A contributing fact that this industry is lagging behind the WebRTC adoption is that there is no standard protocol (like RTSP) designed for ingesting media into a streaming service. WHIP proposes a simple HTTP based protocol that will allow WebRTC based ingest of content into streaming services and/or CDNs.
Frontend
The frontend part uses the WHIP web client SDK (@eyevinn/whip-web-client) which is a library that help you setup a sendonly
WebRTC peer connection to a WHIP compatible media server. Assuming you have an HTMLVideoElement with id webcast
you add the following code to start sending media from your webcam to the WHIP backend.
const videoElement = document.querySelector<HTMLVideoElement>("#webcast");
const client = new WHIPClient({
endpoint: "https://<whip-endpoint>",
element: videoElement,
opts: { iceServers: iceServers }
});
await client.connect();
The iceServers
is an array containing the list of STUN/TURN servers but this is optional as WHIP standard specifies that the receiving peer, WHIP backend, will gather all ICE candidates and include in the SDP answer. However, by providing a list of ICE servers the client may include the candidates in the SDP offer which increases the possibility for an optimal pairing.
That is basically all you need to get started building your browser application for ingesting media to a WHIP compatible media server and / or CDN.
Backend (WHIP endpoint)
In this application we include a WHIP compatible server that has the role of a broadcaster. It provides a WHIP endpoint that the sender uses to establish a connection for media transmission. This incoming media is then forwarded and replicated to all connected watchers. Between the watcher and the broadcast a WebRTC recvonly
peer connection is established. By doing this the sender does not have to establish a peer with all the watcher. This backend is built on the NPM library @eyevinn/whip-endpoint and all you need is the following lines of code.
import { WHIPEndpoint, Broadcaster } from "@eyevinn/whip-endpoint";
const broadcaster = new Broadcaster({
port: parseInt(process.env.BROADCAST_PORT || "8001"),
baseUrl: process.env.BROADCAST_BASEURL,
prefix: process.env.BROADCAST_PREFIX,
iceServers: iceServers,
});
broadcaster.listen();
const endpoint = new WHIPEndpoint({ port: parseInt(process.env.PORT || "8000"), iceServers: iceServers });
endpoint.registerBroadcaster(broadcaster);
endpoint.listen();
On port 8000
you have the WHIP endpoint running and is the endpoint that the sender connects with. All watchers connect to the broadcaster on port 8001 to establish a WebRTC peer connection between the broadcaster and the watcher. This part is not part of the WHIP standard even though it has some similarities how it is done. It is also based on an HTTP endpoint to signal and exchange SDP offers and answers.
In our Simple Webcast application we generate a link that includes a base64 encoded string of an URI to the specific channel on the broadcaster. And to watch we then have the following code where channelUrl
is the URI mentioned.
const peer = new RTCPeerConnection({
iceServers: iceServers
});
peer.onicecandidate = async (event) => {
if (event.candidate === null) {
const response = await fetch(channelUrl, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ sdp: peer.localDescription.sdp })
});
if (response.ok) {
const { sdp } = await response.json();
peer.setRemoteDescription({ type: "answer", sdp: sdp });
}
}
}
peer.ontrack = (ev) => {
if (ev.streams && ev.streams[0]) {
video.srcObject = ev.streams[0];
}
};
const sdpOffer = await peer.createOffer({
offerToReceiveAudio: true,
offerToReceiveVideo: true,
});
peer.setLocalDescription(sdpOffer);
We can then create a Docker image and run the container on ECS and Fargate. It is fairly straightforward but you need ensure that the security group allows UDP traffic to the container, and as the container runs two services (port 8000 and 8001) you need to have two ALB target groups towards the same container. This is not supported in the AWS console but you can configure the ECS service with the AWS CLI to achieve this.
Setup TURN server
In order for all this to practically work you most likely need a TURN server (Traversal Using Relay NAT) to relay network traffic between peers when a direct connection is not possible.
There are several options available both self-hosted and cloud provided services. A self-hosted option is to use the open source COTURN project and running it for example on an EC2 instance on AWS. The quickest way to get this up and running is to spin up an EC2 instance (ubuntu) running a Docker engine. To install Docker engine:
sudo apt-get update
sudo apt-get install ca-certificates curl gnupg lsb-release
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io
sudo groupadd docker
sudo usermod -aG docker ubuntu
Once Docker is installed run the container (for simplicity) in host mode. Running it in host mode simplifies for coturn
to determine relay IP address for example. This docker-compose file can be used as inspiration.
version: "3.7"
services:
coturn:
image: coturn/coturn:latest
network_mode: host
command: -n --log-file=stdout -r <realm> -a -u eyevinn:<credential> --external-ip='<public-ip>/<private-ip>' --min-port=49160 --max-port=49200
In the above example you need to ensure that the security group allows TCP and UDP traffic on port 3478 and UDP traffic on 49160 to 49200. To verify that the TURN server is setup correctly you can use the online test tool here.
Troubleshooting
In Chrome browser there is a handy tool that you access by entering chrome://webrtc-internals
in the address bar. A successful and established connection would look like this.
ICE connection state: new => checking => connected
Connection state: new => connecting => connected
Signaling state: new => have-local-offer => stable
ICE Candidate pair: 85.24.142.180:1025 <=> 16.16.27.107:42818
If signaling state is stable
the SDP offer/answer process has completed and ICE candidates have been exchanged but if no ICE Candidate pair is set there is probably an issue with finding a way for the two peers to connect.
The Simple Webcast is a demo application and not scaled for production usage. See that as an inspiring example that you are welcome to use and build upon an upscaled infrastructure (TURN servers and corresponding network infrastructure).
About Eyevinn Technology
Eyevinn Technology is an independent consultant firm specialized in video and streaming. Independent in a way that we are not commercially tied to any platform or technology vendor.
At Eyevinn, every software developer consultant has a dedicated budget reserved for open source development and contribution to the open source community. This give us room for innovation, team building and personal competence development. And also gives us as a company a way to contribute back to the open source community.
Want to know more about Eyevinn and how it is to work here. Contact us at work@eyevinn.se!
Top comments (0)