Video Stream from Raspberry Pi using Express.js, Socket.io & Pi Camera Connect

In this article, I will be sharing how I live stream my raspberry pi camera feed using ExpressJS and Socket.io for the server and Pi Camera Connect with Socket IO Client on the Raspberry Pi.

Hardware

Raspberry Pi 4

Raspberry Pi 4
Raspberry Pi Camera v2.1
32 GB Memory Card
USB Power Supply (I’m using my USB C phone cable plug to my laptop)

Connect your Pi Camera to your Raspberry Pi

Before starting…

I’m writing my codes using IntelliJ on my Mac (which has Node 14+ installed) And the version of Socket.io i’m using is v2.4.1 which as of writing, there’s already v4.0.1 being released.
(Will work towards upgrading to the latest version in a future post)

For this article, I am creating 2 separate repositories where the first repo will focus on the Socket IO Server for clients to connect. The second repo will focus on streaming the camera feed from your Raspberry Pi to the Server.

I will assume that you have NodeJS installed on your computer and knows how to create new ExpressJS project else you can check out their online docs.

On starting up your new Raspberry Pi, you may refer to the docs here to setup your Raspberry Pi. After which, ssh to your Pi and install NodeJS onto your Pi.

Disclaimer: As I’m working on a personal project aim to stream video feed from raspberry pi to multiple clients, therefore in this article, the codes shown does utilise socket rooms but I have tested only with a single client over the local network.

Server-Side

Let’s start working on the Server! (Project Repo 1 — node-iot-server)

1. On your terminal, navigate to your ExpressJS project root and install the Socket.io by running the following line:

npm install socket.io@2.4.1

2. Create a new file called app-socket.js using the editor of your choice in the project root folder.

module.exports = startupSocketIO = server => {
const io = require('socket.io')(server);

/**
* Listening on io.
*/
io.of("iot").on("connection", socket => {


});

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

return io;
}

3. Edit the file www under the directory /bin using the editor of your choice

4. Include the app-socket.js that you have created earlier

const startupIO = require("../app-socket");

5. Right after the server is listening to the port, include the following code:

/**
* Startup web socket io
*/
startupIO(server);

6. Enter the following in the terminal:

npm start

7. Open your browser of your choice and navigate to http://localhost:3000/

8. Congrats, you have startup your node server with socket.io.

Connections…

Let’s start by filling up what should the server react when the raspberry pi camera connects.

1. Add the following codes right after the importing of Socket.io

const io = require('socket.io')(server);let iotDevices = new Map();//<string, SocketIO.Socket>();
let
rooms = new Map();//<string, <string, SocketIO.Socket>()>();

2. Add the following codes in the .on(connection) code block to display the address of the connection coming in.

let address = socket.handshake.address;
console.log("New client connection established...", address);

3. Add the following codes in the .on(connection) code block to create a room for the raspberry pi camera connected.

//=================================================
// Initialize
//=================================================
socket.on("pi-cam-init", (data) => {
console.log("Camera " + data + " is now online!");

if(!iotDevices.has(data)) {
// Camera socket will join a room given by the id
let
roomName = "room" + data;
socket.join(roomName);

// Add camera client to a room map for easier maintaining
if
(rooms.has(roomName)) {
rooms.get(roomName).set(socket.id, socket);
} else {
rooms.set(roomName, new Map().set(socket.id, socket));
}

// Add camera client to a map for easier maintaining
iotDevices.set(data, socket);

} else if(iotDevices.get(data) !== socket) {
console.log("Camera socket different from map, Adding the new socket into the map.");
let roomName = "room" + data;
iotDevices.get(data).leave(roomName);
socket.join(roomName);
iotDevices.set(data, socket);
}

4. Add the following codes in the .on(connection) code block to forward the camera feed from the raspberry pi to all clients within the room.

//=================================================
// Pi Camera Streaming
//=================================================
socket.on("pi-video-stream", (data, res) => {
let roomName = "room" + data;
socket.to(roomName).emit("consumer-receive-feed", res);
});

5. Add the following codes in the .on(connection) code block to handle the socket rooms & the clients once the raspberry pi disconnects.

//=================================================
// Pi Camera Disconnect
//=================================================
socket.on("pi-disconnect", (data, res) => {
console.log("Disconnect (socket) from pi camera " + address);
let roomName = "room" + data;

// Check room connected clients & remove the all clients
if
(rooms.has(roomName)) {
rooms.delete(roomName);
}

// Broadcast offline status to all clients
socket.to(roomName).emit("pi-terminate-broadcast");

res("Pi camera disconnected from server.")
});

6. Congrats! You have setup your NodeJS Server to handle the connection from your Raspberry Pi.

Let’s continue by filling up what should the server react when clients connects to view the camera feed.

7. Add the following codes in the .on(connection) code block to add the consumer clients to the socket room of choice.

//=================================================
// Consumer Join to Start Watching Stream
//=================================================
socket.on("consumer-start-viewing", (data, res) => {
console.log("Start stream from client " + address + " on pi camera ", data);
let roomName = "room" + data;

let camera;
if(iotDevices.has(data)) {
socket.join(roomName);

// Add web client to a room map for easier maintaining
if
(rooms.has(roomName)) {
rooms.get(roomName).set(socket.id, socket);
} else {
rooms.set(roomName, new Map().set(socket.id, socket));
}

camera = iotDevices.get(data);
camera.emit("new-consumer", socket.id, () => {
console.log("New Consumer has joined " + data + " stream");
});

res("Connect to " + camera.id + " steam");

} else {
res("Camera is not online");
}
});

8. Congrats! You have established what the server should do when you have a consumer client connecting.

9. You can continue to work on what the server should do when the pi camera faces connectivity issues, or the consumer client disconnects from the feed etc.

Full Server Source Code here -> https://bitbucket.org/knivore/node-iot-server/src/master/

Client-Side (Pi-Camera)

Now, let’s move on to our Pi Camera. (Project Repo 2 — node-iot)

1. On your terminal, navigate to your 2nd project root and install the following 2 libraries — Socket.io-client & Pi Camera Connect by running the following:

npm install pi-camera-connect
npm install
socket.io-client@2.4.0

2. After some modifications, your package.json will look something like this:

{
"name": "node-iot",
"version": "1.0.0",
"scripts": {
"start": "node app.js"
},
"dependencies": {
"pi-camera-connect": "^0.3.4",
"socket.io-client": "^2.4.0"
}
}

3. Create a new file called app.js using the editor of your choice in the project root folder with the following codes. (Replace the bold ip address with yours)

const io = require('socket.io-client');
const { StreamCamera, Codec, Flip, SensorMode } = require('pi-camera-connect');
const socket = io.connect('http://xxx.xxx.xxx.xxx:3000/iot');

const streamCamera = new StreamCamera({
codec: Codec.MJPEG,
flip: Flip.Vertical,
sensorMode: SensorMode.Mode6
});


socket.on('connect', () => {
socket.sendBuffer = [];

socket.emit("pi-cam-init", "Cam-1");

console.log("Connected to the server!" + socket.id);
})

socket.on('new-consumer', (data) => {
console.log(data + ' has join the stream');
});

socket.on('consumer-left', (data) => {
console.log(data + ' has left the stream');
});

streamCamera.on('frame', (data) => {
socket.binary(true).emit('pi-video-stream', 'Cam-1', "data:image/jpeg;base64," + data.toString("base64"));
});


async function cameraStartCapture() {
await streamCamera.startCapture();
}

async function cameraStopCapture() {
await streamCamera.stopCapture();
}

cameraStartCapture().then(() => {
console.log('Camera is now capturing');
});

4. Copy the entire repo 2 and paste into your raspberry pi. (For convenience sake, I simply place it on the desktop folder)

5. Open the terminal in your Pi and navigate to the folder and enter the following:

npm start

6. Congrats! Your camera is now connected to your server

Full Pi Camera Source Code here -> https://bitbucket.org/knivore/node-iot/src/master/

Client-Side (Web)

1. Create a HTML web page by connecting to the socket server & the correct event name to view the live stream.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

<title>Preview Stream</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.4.0/socket.io.js" crossorigin="anonymous"></script>
<script>
const socket = io.connect('http://localhost:3000/iot');

socket.on("connect_error", (err) => {
console.log(`connect_error due to ${err.message}`);
});

socket.on('connect', () => {
console.log('Socket connected: ', socket.connected);
socket.sendBuffer = [];

socket.emit('consumer-start-viewing', 'Cam-1', () => {
console.log("New watcher for Cam 1 ");
});

socket.on('consumer-receive-feed', (data, res) => {
console.log("Receiving feed..." + data + " " + res);
$('#play').attr('src',data);
$('#log').text(data);
});
})

</script>
</head>

<body>
<img id="play">
</br>
<div id="log"></div>


</body>
</html>

2. Open the html page with your preferred browser.

3. TADA~ That’s about it!

Conclusion

Hopefully, this article is helpful enough for you to start streaming from your Pi Camera.

There are still many things that can be done.
Examples:
a. You can improve your HTML page by adding in buttons to on/off the stream that triggers the correct socket event name.
b. Add in PM2 into your Raspberry Pi that automatically runs the app.js whenever your pi starts.
c. Setup kiosk mode for your Pi

Do let me know what I did right or wrong or how I can improve in the comments!

Thanks for reading!

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store