Webrtc Server

Dette er eksempel viser hvordan man kan streame indholdet af et html canvas element (en p5js sketch) til en anden maskine. Streaming delen håndteres af WebRTC, og etablering af forbindelsen håndteres ved hjælp af Socket.io.

Opsætning af projekt

Først skal der laves et projekt så node kan finde ud af at køre programmet, og har en package.json fil til at holde styr på projektet og afhængigheder af biblioteksmoduler.

Start med at lave en mappe, som kan indeholde dit projekt. Kald den f.eks. webrtc-server. I denne mappe skal du køre følgende kommando, for at oprette projekt filen package.json.

npm init

Udfyld passende værdier som svar på de spørgsmål programmet stiller. Jeg foreslår at ændre din main fil til server.js.

Dernæst har du mulighed for at installere disse afhængigheder.

npm install express
npm install socket.io

Indsæt en start action i script sektionen.

Du burde nu have en package.json fil, der ser nogenlunde sådan ud:

{
  "name": "webrtccanvasbroadcast",
  "version": "1.0.0",
  "description": "Example of streaming canvas content using WebRTC",
  "main": "server.js",
  "scripts": {
    "start": "node server.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.17.1",
    "socket.io": "^3.1.0"
  }
}

Bemærk de to pakker der er listet under afhængigheder.

Server

De statiske web-resourcer placeres i mappen public, og de gøres tilgængelige på samme måde som i eksemplet med den simple web server.

Derudover består serveren af en række endpoints, der håndterer socket.io beskeder. Dette er nødvendigt for at kunne styre transmission af videostrømmen, da dette ikke er indbygget i WebRTC.

Koden til serveren placeres i filen server.js.

const express = require("express");
const app = express();

let broadcaster;
const port = 4000;

const http = require("http");
const server = http.createServer(app);

const io = require("socket.io")(server);
app.use(express.static(__dirname + "/public"));

io.on("error", e => console.log(e));

io.on("connection", socket => {
  socket.on("broadcaster", () => {
    broadcaster = socket.id;
    console.log(`Broadcaster id: ${broadcaster}`)
    socket.broadcast.emit("broadcaster");
  });
  socket.on("watcher", () => {
    console.log(`Watcher id: ${socket.id}`)
    socket.to(broadcaster).emit("watcher", socket.id);
  });
  socket.on("offer", (id, message) => {
    console.log(`Offer id: ${socket.id}, message: ${message}`)
    socket.to(id).emit("offer", socket.id, message);
  });
  socket.on("answer", (id, message) => {
    console.log(`Answer id: ${socket.id}, message: ${message}`)
    socket.to(id).emit("answer", socket.id, message);
  });
  socket.on("candidate", (id, message) => {
    console.log(`Candidate id: ${socket.id}, message: ${message}`)
    socket.to(id).emit("candidate", socket.id, message);
  });
  socket.on("disconnect", () => {
    console.log(`Disconnect id: ${socket.id}`)
    socket.to(broadcaster).emit("disconnectPeer", socket.id);
  });
});

server.listen(port, () => console.log(`Server is running on port ${port}`));

Når du ønsker at starte serveren, kan det gøres med kommandoen.

npm start

Afsender af video stream

For at have noget at sende afsted laves en sketch ved hjælp af p5js, der streames til modtagerne med WebRTC.

Tegn på canvas

Der laves en sketch, der tegner noget på det canvas element, som skal transmitteres. I eksemplet tegnes blot en rød cirkel med sort omrids på musens position. Dette laves i filen public/broadcast/sketch.js.

let stream;

function setup() {
  // Capture the canvas content as a stream
  const c = createCanvas(400, 400);
  const htmlCanvas = c.elt;
  stream = htmlCanvas.captureStream();

  gotStream(stream);
}

function draw() {
  background(220);
  // Draw a red circle at the position of the mouse
  fill('red');
  strokeWeight(5);
  circle(mouseX, mouseY, 50);
}

Styring af video stream

For at kunne håndtere WebRTC forbindelsen kommunikeres med serveren via Socket.io.

Klient-delen der styrer afsendelsen laves i public/broadcast/webrtc.js

const peerConnections = {};
const config = {
  iceServers: [
    {
      "urls": "stun:stun.l.google.com:19302",
    },
  ]
};

const socket = io.connect(window.location.origin);

socket.on("answer", (id, description) => {
  peerConnections[id].setRemoteDescription(description);
});

socket.on("watcher", id => {
  const peerConnection = new RTCPeerConnection(config);
  peerConnections[id] = peerConnection;

  let stream = window.stream;
  stream.getTracks().forEach(track => peerConnection.addTrack(track, stream));

  peerConnection.onicecandidate = event => {
    if (event.candidate) {
      socket.emit("candidate", id, event.candidate);
    }
  };

  peerConnection
    .createOffer()
    .then(sdp => peerConnection.setLocalDescription(sdp))
    .then(() => {
      socket.emit("offer", id, peerConnection.localDescription);
    });
});

socket.on("candidate", (id, candidate) => {
  peerConnections[id].addIceCandidate(new RTCIceCandidate(candidate));
});

socket.on("disconnectPeer", id => {
  peerConnections[id].close();
  delete peerConnections[id];
});

window.onunload = window.onbeforeunload = () => {
  socket.close();
};

function gotStream(stream) {
  window.stream = stream;
  socket.emit("broadcaster");
}

function handleError(error) {
  console.error("Error: ", error);
}

Visning i browser

Javascript koden kan ikke stå alene. For at kunne eksekvere den i browseren bliver den indsat på en simpel web side i filen public/broadcast/index.html.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Video stream - Broadcaster</title>
    <script src="/p5lib/p5.min.js"></script>
    <script src="/p5lib/p5.sound.min.js"></script>
    <link rel="stylesheet" type="text/css" href="../styles.css">
  </head>
  <body>
    <h1>Video stream - Broadcaster</h1>
    <script src="sketch.js"></script>

    <script src="/socket.io/socket.io.js"></script>
    <script src="webrtc.js"></script>
  </body>
</html>

Bemærk at filen /socket.io/socket.io.js genereres automatisk af serveren når socket.io pakken benyttes.

For at kunne tegne på canvas med p5js i public/broadcast/sketch.js er det nødvendigt at inkludere p5 biblioteksfilerne. Disse er placeret i public/p5lib.

Der benyttes også en smule css som placeres i public/styles.css.

html, body {
  margin: 0;
  padding: 0;
}

canvas {
  display: block;
}

video {
  width: 400px;
  height: 400px;
  background-color: black;
}

Modtager af video stream

Den modtagende ende af videostrømmen laves også som en simpel webside. Det er den man skal se, når man besøger serveren.

Lav filen public/index.html med dette indhold.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Video stream - Viewer</title>
    <link href="/styles.css" rel="stylesheet" />
  </head>
  <body>
    <h1>Video stream - Viewer</h1>
    <video playsinline autoplay muted></video>
    <script src="/socket.io/socket.io.js"></script>
    <script src="/watch.js"></script>
  </body>
</html>

Til at styre modtagelsen af WebRTC strømmen benyttes scriptet public/watch.js med dette indhold.

let peerConnection;
const config = {
  iceServers: [
      { 
        "urls": "stun:stun.l.google.com:19302",
      },
      // { 
      //   "urls": "turn:TURN_IP?transport=tcp",
      //   "username": "TURN_USERNAME",
      //   "credential": "TURN_CREDENTIALS"
      // }
  ]
};

const socket = io.connect(window.location.origin);
const video = document.querySelector("video");

socket.on("offer", (id, description) => {
  peerConnection = new RTCPeerConnection(config);
  peerConnection
    .setRemoteDescription(description)
    .then(() => peerConnection.createAnswer())
    .then(sdp => peerConnection.setLocalDescription(sdp))
    .then(() => {
      socket.emit("answer", id, peerConnection.localDescription);
    });
  peerConnection.ontrack = event => {
    video.srcObject = event.streams[0];
  };
  peerConnection.onicecandidate = event => {
    if (event.candidate) {
      socket.emit("candidate", id, event.candidate);
    }
  };
});

socket.on("candidate", (id, candidate) => {
  peerConnection
    .addIceCandidate(new RTCIceCandidate(candidate))
    .catch(e => console.error(e));
});

socket.on("connect", () => {
  socket.emit("watcher");
});

socket.on("broadcaster", () => {
  socket.emit("watcher");
});

window.onunload = window.onbeforeunload = () => {
  socket.close();
  peerConnection.close();
};

Afprøvning

Nu burde du kunne start serveren med kommandoen.

npm start

For at starte transmissionen skal åbne http://localhost:4000/broadcast.

Hvis du derefter besøger http://localhost:4000 i et andet browservindue, kan du se en video transmission af din sketch.

Screenshot af to browserviduer.

Screenshot af de to browserviduer med sender og modtager.

Bemærk at videoen stopper, hvis du lukker det første browservindue. Den burde starte igen, næste gang du besøger http://localhost:4000/broadcast.

Materiale