cft
Become a CreatorSign inGet Started

Build a Peer-to-Peer (P2P) Image Sharing App with WebRTC and React

Allow users to share images directly with each other, eliminating the need for an intermediary server


user

Eyuel Berga Woldemichael

4 months ago | 16 min read
Follow

build-peer-to-peer-p2p-image-rwfir

WebRTC makes it possible to transfer files between two browsers, eliminating the need to upload a file to a server before sharing. Data is not stored in an intermediary server, which makes the transfer fast and secure. In this article, we will build a simple React app that will allow users to share images directly with each other using WebRTC.

A demo of what we will be building:

The final app

To start, we need a signaling server. This is used for establishing a connection between peers. The signaling server doesn’t deal with the media traffic, it is only responsible for enabling users to find each other in the network, establishing the connection, resetting, and closing it down.

Let's start by creating a new project with an Express server and Socket.IO because we want our signaling server to use a WebSocket connection.

$ yarn init

$ yarn add express socket.io username-generator

Now create an index.js file that will be the entry point for our application. Start by importing the libraries and setting up our server:

const path = require('path')

const usernameGen = require("username-generator");

const express = require('express')

const app = express()

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

const io = require("socket.io")(http, {

cors: {

origin: "*",

},

})

Generally, our server needs to listen for events from the client and also emit another event in response. We define all these events in a variable called SOCKET_EVENT

const SOCKET_EVENT = {

CONNECTED: "connected",

DISCONNECTED: "disconnect",

USERS_LIST: "users_list",

REQUEST_SENT: "request_sent",

REQUEST_ACCEPTED: "request_accepted",

REQUEST_REJECTED: "request_rejected",

SEND_REQUEST: "send_request",

ACCEPT_REQUEST: "accept_request",

REJECT_REQUEST: "reject_request",

};

We also need a variable to store all users that are connected. We do that on the users object. We also assign random usernames for all connected users with the help of the username-generator library.

We also define two helper functions, usersList which converts the users object into an array and logger, which is just for logging into the console, so we can see what is happening inside.

// the users object

const users = {};

// converts users into a list

const usersList = (usersObj)=>{

const list = [];

Object.keys(usersObj).forEach(username=>{

list.push({username, timestamp:usersObj[username].timestamp});

})

return list;

};

// console log with timestamp

function Log(message, data){

console.log((new Date()).toISOString(),message, data);

};

Okay, and now the main part, establishing a connection between peers. What we are basically doing here is relaying messages from the two users, so that they can establish a peer-to-peer connection.

io.on("connection", (socket) => {

//generate username against a socket connection and store it

const username = usernameGen.generateUsername("-");

if (!users[username]) {

users[username] = { id: socket.id, timestamp: new Date().toISOString() };

}

logger.log(SOCKET_EVENT.CONNECTED, username);

// send back username

socket.emit(SOCKET_EVENT.CONNECTED, username);

// send online users list

io.sockets.emit(SOCKET_EVENT.USERS_LIST, usersList(users) );

socket.on(SOCKET_EVENT.DISCONNECTED, () => {

// remove user from the list

delete users[username];

// send current users list

io.sockets.emit(SOCKET_EVENT.USERS_LIST, usersList(users) );

Log(SOCKET_EVENT.DISCONNECTED, username);

});

socket.on(SOCKET_EVENT.SEND_REQUEST, ({ username, signal, to }) => {

// tell user that a request has been sent

io.to(users[to].id).emit(SOCKET_EVENT.REQUEST_SENT, {

signal,

username,

});

Log(SOCKET_EVENT.SEND_REQUEST, username);

});

socket.on(SOCKET_EVENT.ACCEPT_REQUEST, ({ signal, to }) => {

// tell user the request has been accepted

io.to(users[to].id).emit(SOCKET_EVENT.REQUEST_ACCEPTED, {signal});

Log(SOCKET_EVENT.ACCEPT_REQUEST, username);

});

socket.on(SOCKET_EVENT.REJECT_REQUEST, ({ to }) => {

// tell user the request has been rejected

io.to(users[to].id).emit(SOCKET_EVENT.REQUEST_REJECTED);

Log(SOCKET_EVENT.REJECT_REQUEST, username);

});

});

const port = process.env.PORT || 7000;

http.listen(port);

Log("server listening on port", port);

The server part is done, we can move on to the front-end. We start by creating a new react app:

$ create-react-app pic-share

We also need to add some additional packages. simple-peer makes it easier to work with the WebRTC API. We will also use Bulma for styling.

$ yarn add simple-peer react-bulma-components react-dropzone socket.io-client

All the WebRTC related logic will be placed in the App.js file. We will use some custom components here, which are available from the source code. Let's define all the states and references we need to get started.

import React, { useRef, useEffect, useState } from "react";

import "react-bulma-components/dist/react-bulma-components.min.css";

import "./components/EmptyPlaceholder.css";

import {

Container,

Columns,

Image,

Navbar,

Modal

} from "react-bulma-components/dist";

import UserInfo from "./components/UserInfo";

import ShareRequest from "./components/ShareRequest";

import ImageUploader from "./components/ImageUploader";

import EmptyPlaceholder from "./components/EmptyPlaceholder";

import Loader from "./components/Loader";

import logo from "./logo.png";

import io from "socket.io-client";

import Peer from "simple-peer";

function App() {

const socket = useRef();

const peerInstance = useRef();

const [requested, setRequested] = useState(false);

const [sentRequest, setSentRequest] = useState(false);

const [sending, setSending] = useState(false);

const [receiving, setReceiving] = useState(false);

const [rejected, setRejected] = useState(false);

const [loading, setLoading] = useState(false);

const [myUsername, setMyUsername] = useState("");

const [usersList, setUsersList] = useState([]);

const [peerUsername, setPeerUsername] = useState("");

const [peerSignal, setPeerSignal] = useState("");

const [file, setFile] = useState(null);

const [receivedFilePreview, setReceivedFilePreview] = useState("");

const SOCKET_EVENT = {

CONNECTED: "connected",

DISCONNECTED: "disconnect",

USERS_LIST: "users_list",

REQUEST_SENT: "request_sent",

REQUEST_ACCEPTED: "request_accepted",

REQUEST_REJECTED: "request_rejected",

SEND_REQUEST: "send_request",

ACCEPT_REQUEST: "accept_request",

REJECT_REQUEST: "reject_request"

};

return (

<React.Fragment>

</React.Fragment>

);

}

export default App;

We need to listen to events emitted from the signaling server and handle them. Let's do that on a useEffect hook. Note that we are providing an empty array for the second parameter because we don’t want it to run on each update.

const SERVER_URL = "/";

useEffect(() => {

socket.current = io.connect(SERVER_URL);

socket.current.on(SOCKET_EVENT.CONNECTED, username => {

setMyUsername(username);

});

socket.current.on(SOCKET_EVENT.USERS_LIST, users => {

setUsersList(users);

});

socket.current.on(SOCKET_EVENT.REQUEST_SENT, ({ signal, username }) => {

setPeerUsername(username);

setPeerSignal(signal);

setRequested(true);

});

socket.current.on(SOCKET_EVENT.REQUEST_ACCEPTED, ({ signal }) => {

peerInstance.current.signal(signal);

});

socket.current.on(SOCKET_EVENT.REQUEST_REJECTED, () => {

setSentRequest(false);

setRejected(true);

});

}, []);

Now, let's define methods to handle the main functions of the app, which are sending, accepting, and rejecting requests.

// accept request sent from peer

const acceptRequest = () => {

setRequested(false);

const peer = new Peer({

initiator: false,

trickle: false

});

peer.on("signal", data => {

socket.current.emit(SOCKET_EVENT.ACCEPT_REQUEST, {

signal: data,

to: peerUsername

});

});

peer.on("connect", () => {

setReceiving(true);

});

const fileChunks = [];

peer.on("data", data => {

if (data.toString() === "EOF") {

// Once, all the chunks are received, combine them to form a Blob

const file = new Blob(fileChunks);

setReceivedFilePreview(URL.createObjectURL(file));

setReceiving(false);

} else {

// Keep appending various file chunks

fileChunks.push(data);

}

});

peer.signal(peerSignal);

peerInstance.current = peer;

};

// reject request from peer

const rejectRequest = () => {

socket.current.emit(SOCKET_EVENT.REJECT_REQUEST, { to: peerUsername });

setRequested(false);

};

//send request to peer

const sendRequest = username => {

setLoading(true);

setPeerUsername(username);

const peer = new Peer({

initiator: true,

trickle: false

});

peer.on("signal", data => {

socket.current.emit(SOCKET_EVENT.SEND_REQUEST, {

to: username,

signal: data,

username: myUsername

});

setSentRequest(true);

setLoading(false);

});

peer.on("connect", async () => {

setSending(true);

setSentRequest(false);

let buffer = await file.arrayBuffer();

const chunkSize = 16 * 1024;

while (buffer.byteLength) {

const chunk = buffer.slice(0, chunkSize);

buffer = buffer.slice(chunkSize, buffer.byteLength);

// Off goes the chunk!

peer.send(chunk);

}

peer.send("EOF");

setSending(false);

});

peerInstance.current = peer;

};

When a user sends a request, a signal is sent to the user. The sender waits until a connection is made, then it starts sending the image in chucks. When all chucks are sent, the user sends EOF indicating the end of the file. On the receiver side, the user receives all the chucks and combines them into a blob, forming the complete image.

Our final App.js file will look something like this:

import React, { useRef, useEffect, useState } from "react";

import "react-bulma-components/dist/react-bulma-components.min.css";

import "./components/EmptyPlaceholder.css";

import {

Container,

Columns,

Image,

Navbar,

Modal

} from "react-bulma-components/dist";

import UserInfo from "./components/UserInfo";

import ShareRequest from "./components/ShareRequest";

import ImageUploader from "./components/ImageUploader";

import EmptyPlaceholder from "./components/EmptyPlaceholder";

import Loader from "./components/Loader";

import logo from "./logo.png";

import io from "socket.io-client";

import Peer from "simple-peer";

function App() {

const socket = useRef();

const peerInstance = useRef();

const [requested, setRequested] = useState(false);

const [sentRequest, setSentRequest] = useState(false);

const [sending, setSending] = useState(false);

const [receiving, setReceiving] = useState(false);

const [rejected, setRejected] = useState(false);

const [loading, setLoading] = useState(false);

const [myUsername, setMyUsername] = useState("");

const [usersList, setUsersList] = useState([]);

const [peerUsername, setPeerUsername] = useState("");

const [peerSignal, setPeerSignal] = useState("");

const [file, setFile] = useState(null);

const [receivedFilePreview, setReceivedFilePreview] = useState("");

const SOCKET_EVENT = {

CONNECTED: "connected",

DISCONNECTED: "disconnect",

USERS_LIST: "users_list",

REQUEST_SENT: "request_sent",

REQUEST_ACCEPTED: "request_accepted",

REQUEST_REJECTED: "request_rejected",

SEND_REQUEST: "send_request",

ACCEPT_REQUEST: "accept_request",

REJECT_REQUEST: "reject_request"

};

const peerConfig = {

iceServers: [

{ urls: "stun:stun.l.google.com:19302" },

{ urls: "stun:stun1.l.google.com:19302" },

{ urls: "stun:stun2.l.google.com:19302" },

{ urls: "stun:stun3.l.google.com:19302" },

{ urls: "stun:stun4.l.google.com:19302" }

]

};

const acceptRequest = () => {

setRequested(false);

const peer = new Peer({

initiator: false,

trickle: false

});

peer.on("signal", data => {

socket.current.emit(SOCKET_EVENT.ACCEPT_REQUEST, {

signal: data,

to: peerUsername

});

});

peer.on("connect", () => {

setReceiving(true);

});

const fileChunks = [];

peer.on("data", data => {

if (data.toString() === "EOF") {

// Once, all the chunks are received, combine them to form a Blob

const file = new Blob(fileChunks);

setReceivedFilePreview(URL.createObjectURL(file));

setReceiving(false);

} else {

// Keep appending various file chunks

fileChunks.push(data);

}

});

peer.signal(peerSignal);

peerInstance.current = peer;

};

const rejectRequest = () => {

socket.current.emit(SOCKET_EVENT.REJECT_REQUEST, { to: peerUsername });

setRequested(false);

};

const sendRequest = username => {

setLoading(true);

setPeerUsername(username);

const peer = new Peer({

initiator: true,

trickle: false,

config: peerConfig

});

peer.on("signal", data => {

socket.current.emit(SOCKET_EVENT.SEND_REQUEST, {

to: username,

signal: data,

username: myUsername

});

setSentRequest(true);

setLoading(false);

});

peer.on("connect", async () => {

setSending(true);

setSentRequest(false);

let buffer = await file.arrayBuffer();

const chunkSize = 16 * 1024;

while (buffer.byteLength) {

const chunk = buffer.slice(0, chunkSize);

buffer = buffer.slice(chunkSize, buffer.byteLength);

// Off goes the chunk!

peer.send(chunk);

}

peer.send("EOF");

setSending(false);

});

peerInstance.current = peer;

};

const SERVER_URL = "/";

useEffect(() => {

socket.current = io.connect(SERVER_URL);

socket.current.on(SOCKET_EVENT.CONNECTED, username => {

setMyUsername(username);

});

socket.current.on(SOCKET_EVENT.USERS_LIST, users => {

setUsersList(users);

});

socket.current.on(SOCKET_EVENT.REQUEST_SENT, ({ signal, username }) => {

setPeerUsername(username);

setPeerSignal(signal);

setRequested(true);

});

socket.current.on(SOCKET_EVENT.REQUEST_ACCEPTED, ({ signal }) => {

peerInstance.current.signal(signal);

});

socket.current.on(SOCKET_EVENT.REQUEST_REJECTED, () => {

setSentRequest(false);

setRejected(true);

});

}, []);

useEffect(

() => () => {

// Make sure to revoke the data uris to avoid memory leaks

URL.revokeObjectURL(receivedFilePreview);

},

[receivedFilePreview]

);

return (

<React.Fragment>

<Navbar fixed="top" active={false} transparent>

<Navbar.Brand>

<Navbar.Item renderAs="a" href="#">

<img src={logo} alt="Pic Share" />

</Navbar.Item>

<Navbar.Burger />

</Navbar.Brand>

</Navbar>

<Modal

show={

receivedFilePreview !== "" ||

sending ||

receiving ||

sentRequest ||

rejected ||

requested

}

onClose={() => {

if (!sending || !receiving || !sentRequest || !requested)

setReceivedFilePreview("");

setRejected(false);

}}

>

<Modal.Content>

{requested && (

<ShareRequest

acceptRequest={acceptRequest}

rejectRequest={rejectRequest}

peerUsername={peerUsername}

/>

)}

{(sending || receiving || sentRequest) && (

<Loader

text={

sending

? "the picture is being sent, please wait..."

: sentRequest

? "Wait till user accepts your request"

: "receiving picture, please wait... "

}

/>

)}

{rejected && (

<UserInfo

myUsername={peerUsername}

subtext={`${peerUsername} Rejected your request, sorry!`}

color="#ffcac8"

/>

)}

{receivedFilePreview && (

<React.Fragment>

<UserInfo

myUsername={peerUsername}

subtext={`${peerUsername} has sent you this image`}

color="#c7ffcc"

/>

<Image src={receivedFilePreview} />

</React.Fragment>

)}

</Modal.Content>

</Modal>

<Container fluid>

<Columns>

<Columns.Column size="three-fifths">

<UserInfo

myUsername={myUsername}

subtext="Share your username with others so they can send you a picture"

color="#EFFFFF"

/>

<ImageUploader setFile={setFile} />

</Columns.Column>

<Columns.Column>

{usersList.length > 1 ? (

usersList.map(

({ username, timestamp }) =>

username !== myUsername && (

<UserInfo

key={username}

myUsername={username}

timestamp={timestamp}

sendRequest={sendRequest}

disabled={!file || loading}

/>

)

)

) : (

<EmptyPlaceholder

title="No Users Online Right Now!"

subtitle="Wait till someone connects to start sharing"

/>

)}

</Columns.Column>

</Columns>

</Container>

</React.Fragment>

);

}

export default App;

Conclusion

That's all for this article, hope you enjoyed reading it. Check out the source code and demo and please share your comments or suggestions in the comment section.

Upvote


user
Created by

Eyuel Berga Woldemichael

Follow

Software Developer

I am a Software Developer currently based in Addis Ababa, Ethiopia. I am passionate about Web and Mobile Development.


people
Post

Upvote

Downvote

Comment

Bookmark

Share


Related Articles