Hi, in this post I will be covering only WebRTC simple setup for streaming using getDisplayMedia() (screen) but is also valid for getUserMedia() (camera, microphone).

First if you are intereseted in more WebRTC explanations/tutorials this is part of a serie of WebRTC articles for JavaScript and Golang (This includes general knowledge about WebRTC).

In second place I will introduce you the fundamentals about MediaChannels , what they are and how to use them in your webapp.

In a Peerconnection we can create two types of channels, DataChannels (used for passing general purpose data between peers) and MediaChannels (for media data like video/audio). The MediaChannels are negociated before the stablish of the connection but its paremeters can change with the time automatically or manually.

The streaming using MediaChannels usually work well just 📦 out of the box but if your webapp needs specific behaviour that differs from the default you can make use of the video/audio codecs.

The are a lot of codecs to use but you need to know that not all codecs are always supported (they are dependent to the software/hardware of the user) but no worries because the codec negociation is handled by the browser like magic 👌. Here you have a list of the codecs available for WebRTC.

To work with codecs we will need an Array of prefered codecs ordered in preferency. Ex: [The codec I want the most, ..., Other codecs, ..., Worst codec]. The codecs preferences must be set by the client (the one that want to receive the video/audio).

Now that you have learned this little general information about MediaChannels we will see some examples.

⭐ This are modified and simplified examples of "LibreRemotePlay" my software product that tries to be an alternative to Steam Remote Play. If you are interested to learn more about how it works I will continue this serie, but for the moment you will have the codebase to check it out

LibreRemotePlay logo banner

📘 Code example for Host Peer (who shares screen)

In this example signalization is made using a WebSocket, I call it ws()

Next we will write all the logic for our JS host

// PeerConnection declaration (note that you will need to specify iceServers)
let peerConnection = peerConnection = new RTCPeerConnection({iceServers});;

// This function returns the media stream generated by getDisplayMedia or
// undefined if the user rejects to share screen.
// Video and Audio properties can be objects to specify some parameters
// like resolution, frame rate, ...
async function getDisplayMediaStream(resolution, idealFrameRate maxFramerate) {
    try {
        const mediastream = await navigator.mediaDevices.getDisplayMedia({
            video: true,
            audio: true,
        });

        return mediastream;
    } catch (e) {
        return undefined;
    }
}

peerConnection.onicecandidate = async (event) => {
    if (event.candidate) {
        const data = {
            type: 'candidate',
            candidate: event.candidate.toJSON(),
            role: 'host'
        };

        return ws().send(JSON.stringify(data));

    }

    const answer = peerConnection?.localDescription?.toJSON();
    const data = {
        type: 'answer',
        answer,
        role: 'host'
    };

    return ws().send(JSON.stringify(data));

};

let offerArrived = false

async function onSignalArrive(data) {

    const { type, offer, candidate, role } = JSON.parse(data);

    if (role !== 'client') return;

    if (type === "candidate") {
        try {peerConnection.addIceCandidate(candidate)} catch {/** */}
        return
    }

    if (type !== 'offer') return;
    if (!offer || offerArrived) return;

    try {
        await peerConnection.setRemoteDescription(offer);
    } catch (e) {
        console.error(e)
        return
    }
    offerArrived = true;

    const stream = await getDisplayMediaStream(resolution, idealFrameRate, maxFramerate);

    // Here I add MediaStream tracks to the WebRTC Peerconnection
    stream?.getTracks().forEach((track) => {
        if (!stream) return;
        peerConnection?.addTrack(track, stream);
    });

    try {

        await peerConnection.setLocalDescription(await peerConnection.createAnswer());

    } catch (e) {
        console.error(e)
        return
    }

}


const cllbck = (ev) =>  onSignalArrive(ev.data)
ws().addEventListener("message", cllbck)
unlistenerStreamingSignal = () => ws().removeEventListener("message", cllbck)
return

📘 Code example for Client Peer (who receives video/audio stream)

In this example signalization is made using a WebSocket, I call it ws()

Declaring HTML video element

id="video-streaming">

Declaring our prefered codecs

const DEFAULT_PREFERED_CODECS = ["video/VP9","video/AV1","video/H264", "video/VP8"]

The order of preference is: "VP9", "AV1", "H264" and "VP8"

First we will declare a function to get our prefered codecs in order

// This function is a little modification of one that you can find in MDN
// docs
export function getSortedVideoCodecs() {

    const codecs = RTCRtpReceiver.getCapabilities("video")?.codecs;

    if (!codecs) return [];

    return codecs.sort((a, b) => {
      const indexA = DEFAULT_PREFERED_CODECS.value.indexOf(a.mimeType);
      const indexB = DEFAULT_PREFERED_CODECS.value.indexOf(b.mimeType);
      const orderA = indexA >= 0 ? indexA : Number.MAX_VALUE;
      const orderB = indexB >= 0 ? indexB : Number.MAX_VALUE;
      return orderA - orderB;
    });
}

And now all the logic for the JS client

// PeerConnection declaration (note that you will need to specify iceServers)
let peerConnection = peerConnection = new RTCPeerConnection({iceServers});;

// Get the video element
const videoElement = document.getElementById("video-streaming");

// Add transceiver to receive video (if you don't want codec preferences
// is not needed to declare the transceiver)
const transceiver = peerConnection.addTransceiver("video", { direction: "recvonly" });

// Set the codec preferences to the transceiver (this is the prefered
// syntax, a lot of StackOverflow answers are not updated and say that 
// you must modify the raw SDP messages. These days setCodecPreferences
// is supported in almost every modern browsers)
transceiver.setCodecPreferences(getSortedVideoCodecs());

peerConnection.onicecandidate = (e) => {
    if (!e.candidate) return;

    const data = {
        type: 'candidate',
        candidate: e.candidate.toJSON(),
        role: 'client'
    };

    ws().send(JSON.stringify(data));
};

peerConnection.ontrack = (ev) => {

    if (ev.streams && ev.streams[0]) {
        // Setting videoElement source and automatically play video
        videoElement.srcObject = ev.streams[0];
        videoElement.play();
    } else {
        if (!inboundStream) {
        // Setting videoElement source and automatically play video
            inboundStream = new MediaStream();
            videoElement.srcObject = inboundStream;
            videoElement.play();
        }
        // Adding the track to the MediaStream
        inboundStream.addTrack(ev.track);
};

// Create the offer indicating that we want to receive audio/video
const offer = await peerConnection.createOffer({
    offerToReceiveAudio: true,
    offerToReceiveVideo: true,
    iceRestart: true
});

await peerConnection.setLocalDescription(offer);

const data = {
    type: 'offer',
    offer: offer,
    role: 'client'
};

ws().send(JSON.stringify(data));

async function onSignalArrive(data) {
    const { type, answer, candidate, role } = JSON.parse(data);

    if (role !== 'host') {
        return;
    }

    switch (type) {
        case 'answer':
            if (!answer) return;
            await peerConnection.setRemoteDescription(answer);
            break;
        case 'candidate':
            try {await peerConnection.addIceCandidate(candidate)} catch {/** */}
            break;
    }
}

const cllbck = (ev) =>  onSignalArrive(ev.data);
ws().addEventListener("message", cllbck);
unlistenerStreamingSignal = () => ws().removeEventListener("message", cllbck);

With this code you can start implementing your own video streaming solution, I hope you liked this little tutorial. ❤

If you want more WebRTC tutorials tell me at the comments I will read them 👀.

Sources of information: