How to Send a File Using WebRTC Data API

There’s more to WebRTC than voice and video calling. You can even use it to send files across browsers.

Hadar Weiss[Hadar Weiss (@whadar) is CTO and Founder at Peer5 which runs sharefest.me. I’ve had several interesting chats with him, and I wanted to have him here as a guest. In this first post by Hadar, he will explain how to use the WebRTC data channel to send a file.]

WebRTC Data (aka DataChannels / RTCDataChannel) API is letting developers transmit arbitrary data directly between two users (P2P) in ultra low latency. This is something that was not possible up until recently, and it’s a game changer. Why? Because in the next few years it will be an important building block for building web applications.

The most obvious, and talked about applications are web conferencing and file sharing. Traditionally, file-sharing and video chat were built using P2P technology that relied on proprietary clients (BitTorrent, Skype). Due to several limitations, it was the best (or the only) choice for the mainstream. But once WebRTC matures, the natural, long-awaited evolution (not revolution in my opinion) will prevail.

Being on the web brings so many powerful advantages. The web is standard, more updatable, accessible, searchable, embeddable and is just plain awesome.

201308-the-web

But the best part is that being a p2p web application means that your app can work out-of-the-box from any modern browser (now more than 1B endpoints) without installation.

Think about it for a moment… 1,000,000,000+ endpoints.

But there are few things that need to happen before this dream come true. For once, WebRTC Data API is still very limited, incomplete and fairly unsupported by browsers. For example, Chrome and Firefox cannot send data to each other. Binary data is not yet supported on Chrome, but only on recent Canary versions with the inclusion of SCTP. Safari and IE are still with no clear roadmap for WebRTC.

The second obstacle is that many essential complementary services are still under development and maturing – Signaling, NAT traversal and fault tolerance backends. And when doing many-to-many file sharing there’s more stuff you need to take care of which relate to distributed computing – matching of the peers, load distribution, data authentication, streaming throttling and resource preservation. But wait, we’ll get to this on another blog post…

Let’s dive into how data API works and what it takes from a web developer to build basic one-to-one filesharing app. Although there’s an additional complexity of dealing with files, I’m going to use Sharefest as a reference, since it’s a real world, open source project that represents a familiar use case – sending a large file to somebody.

Sending a file

Sharefest works using many-to-many swarming algorithm (or mesh network), where more than two users (peers) can consume a file efficiently. I will try to ignore this and focus on the actual steps that are necessary even in the simplest 1-to-1 scenario.

Step 1: Read the file into JS space

Your file is in your filesystem and we need to make it accessible in your browser, so we can start using it. File API for the rescue:

In the index.html we have

When clicking (or dragging) files, addFiles will be called with the selected files metadata.

Currently in Sharefest, we are only dealing with one file at a time (help us to improve this), so we just read the first file:

Step 2: Chunkify (and possibly also blockify)

We were able to read the file in JS, but it doesn’t mean that we can send it with the Data API. The API doesn’t split large chunks of data, and requires the developer to do this instead. From the specs, the MTU size is defined to be 1280 bytes. From our experiments, it is better to send packets under 1200 bytes to make sure there are no drops. We call these atomic pieces of data, chunks.

So basically all we have to do is to slice our long file into these small chunks.

It is useful to have another level of data encapsulation, blocks, which contain multiple chunks. Block-level verification and availability bitmap of blocks (which is sent to other peers) is sufficient and saves us a lot of bandwidth and CPU (VS. if we had only chunks). Our blockMap abstracts the blocks and helps to read and write the chunks to local storage.

(or blockMap.get(swarmId).getChunk(chunkId)  to write it on the receiver side)

Step 3: Signalling

Our sender has its file split into chunks, and is now ready to be connected to other peers. But as you know, you need a server in the middle to initiate the connection. We use node.js and ws for simple and effective signalling. This area was covered in many other blogs and I encourage you to read about all nuances.

In the simple 1 to 1 sending scenario, the server will simply send the metadata encapsulated in a match object:  sender.send(peerId, new protocol.Match(swarm.id, peer));

Where the Match class is:

Note that there are no network descriptors such as IP address. In the case of 1-to-1 sharing, using this class may be an overhead. We can usually assume a single swarm, peerId (GUID) doesn’t matter, and the availablilityMap would always be full or empty.

On the client side, we decode this message and create a PeerConnectionImpl which wraps the RTCPeerConnection object and create a coherent implementation to the client on both Firefox and Chrome. For example, this is how the instantiation of the actual PeerConnection object looks like:

Then we start the call:

WebRTC (the browser) is going to call our setLocalAndSendMessage with the SDP https://hacks.mozilla.org/2013/07/webrtc-and-the-ocean-of-acronyms/#sdp

And we will wrap its SDP text with our protocol message:

This will ensure the server will be able to route the SDP message to the correct target.

The answer is pretty much similar and also very similar to standard WebRTC flow (see createAnswer).

Step 4: ICE or just STUN

ICE stands for Interactive Connectivity Establishment. It enables P2P in various network conditions that involve NATs and firewalls. ICE incorporates STUN and TURN. While smart people can elaborate on this matter, I rather dumb it down to STUN = Real P2P, TURN = P2P Thru Relay.

In some scenarios, you may decide not to use TURN and restrict only to STUN. This will ensure no one is in the middle – which may improve speeds, security and operating costs.

This servers list goes as param to RTCPeerConnection constructor.

Step 5: Actual P2P Data

Each chunk contains some metadata, and is encoded using our protocol:

Chunk are sent using this Data object:

BinaryProtocol.encode takes a data object and serialize it to UInt8Array. Until binary channel is available on stable browsers, we are forced to use hackish base64 encoding and send text instead of binary. This is how our wrapper handles it:

The receiver side is wired to handle these messages:

The dataReceivedEvent is published using radio.js and eventually written to the chunks dictionary (as described before).

Because it is one-to-one sharing and I wanted to keep it simple I have omitted some of our protocol message that control the flow of the transmission: HAVE, REQUEST, CANCEL which are implemented here.

Step 5.5: Optimizing speed

The data channel spec suggests both reliable and unreliable modes, Firefox implemented both and Chrome has unreliable implemented and plans to implement reliable. In order to optimize transfer speeds, unreliability is key. The latency overheads incorporated in reliable transport directly damage the speed in which data is transferred. Also, some applications don’t even need to reliably transfer data (i.e video conference).

We have planned from the beginning for unreliable data channels. But file transfer still needs to have the entire file and certainly all the chunks of the file. Understanding that, one solution is that the receiver side will need to request chunks that it doesn’t have and monitor those chunks in case the request wasn’t answered.

So we need a data structure to know which chunks are requested and haven’t yet arrived: p2pPendingChunks, so we won’t request them again and again. and we need to monitor and decide when a request has dropped, and then decide what to do with it.

The expiration duration – how long we will wait for a request until we consider it dropped, can vary as a rule of thumb from tests we ran, we configured it to 1500ms. But it should be dynamic.

Another issue need to be taken care of is flow-control. The transfer rate over a certain channel between 2 peers is limited, and varies as congestion over that channel varies. Sending data over that channel in a rate higher than the current limit will cause packet loss and effectively slow down your user’s application, other applications and even other users. Data channels spec describes SCTP as the underlying flow-control mechanism. BUT (this is getting old already) Chrome hasn’t yet implemented it. Firefox has implemented it but they don’t expose the outgoing buffer, thus giving the application no feedback on the state of the channel.

Either way, whether Firefox or Chrome, the application needs to control its flow and we do it via the packet drop mechanism described above. Meaning, when there are lots of dropped packets the transfer rate should go down, and vice versa. The flow control mechanism we’ve implemented is a bit like TCP’s: first of all each request a receiver makes can contain many chunks, so each request can be answered with many DATA messages. second thing, we keep a window of the maximum number of pending chunks we allow at any given time, and monitor the number of pending chunks at any given time (remember the pendingChunks data structure).

So when a request is answered with no dropped chunks we will increase by 1 the size of the window. And when there are dropped chunks we will decrease the window by 2*(number of dropped chunks) and to no more than window/2, being a bit more promiscuous than TCP.

This heuristic worked for us quite well, but of course there’s room for improvement and there are many flow algorithms out there, e.g SCTP, which also change the expiration duration.

One last thing on this topic, since Chrome hasn’t yet implemented flow-control (as to version M29 and below)  they hard coded a speed limitation to 30kb/s using the SDP ‘b’ parameter field. In order to remove that limitation we’ll need to deploy this little hack when handling the SDP signaling when opening a peer connection:

To understand more check out this issue: https://github.com/Peer5/ShareFest/issues/10 (thanks to Justin Uberti).

Step 6: Downloading to regular FS

It’s nice that we have all these data chunks in our JS, but unfortunately humans can’t use it. We need to “Download” (although it’s already local) it to the user and make it accessible:

This is how Sharefest handles files, but we are changing it because creating large UInt8Array in memory is heavy and limited to several hundred MBs. But for the simple case the above method should be sufficient.

Few words on security

This basic scenario assumes both sides are authenticated and use encrypted client-server communication (we use HTTPS+WSS which are based on TLS). It is best practice to verify using SHA3 or other good hashing algorithm, that the file you’ve downloaded is really the file you should have. I’m going to elaborate more on that on a future post.

-
If you are looking for more posts on WebRTC, you are invited to check my WebRTC post series.

Tags: , , , , , , , , , , , , , , , , , , , , , , , , , , , ,

Liked this post?

Share it!

Never miss a post!

Or just grab the RSS feed!

Comments

  1. Hi,
    is it also possible to stream an audiofile from one browser to many ?
    How about streaming an audio file for nodejs to many clients using webrtc?

  2. Ivan Musko says:

    With most tools it is pain in the bu** to transfer big files over the internet. Another option is to transfer with Binfer. It is able to transfer big files of any size.

  3. Do you have this example all packaged together to be able to play with it or see it “all together?”

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">