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:

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.