We are going to cover the Stream API in order to understand WebRTC in the next lesson.

Using streams we can receive a resource from the network, or from other sources, and process it as soon as the first bit arrives.

Instead of waiting for the resource to completely download before using it, we can immediately work with it.

The first example that comes to mind when talking about streams is loading a YouTube video - you don’t have to fully load it before you can start watching it. Or live streaming, where you don’t even know when the content will end. The content does not even have to end. It could be generated indefinitely.

The Stream API allows us to work with this kind of content.

We have 2 different streaming modes: reading from a stream, and writing to a stream. Readable streams are available in all modern browsers except Internet Explorer, and that’s what we’ll talk about here.

As always, check caniuse.com for the most up-to-date information on this matter.

We have 3 classes of objects when it comes to readable streams:

  • ReadableStream
  • ReadableStreamDefaultReader
  • ReadableStreamDefaultController

We can consume a stream using a ReadableStream object.

Here is the first example of a readable stream. The Fetch API allows to get a resource from the network and make it available as a stream:

(async () => {
  const stream = (await fetch('/')).body
})()

I wrapped this in an async immediately-invoked function to use await.

The body property of the fetch response is a ReadableStream object instance. This is our readable stream.

Calling getReader() on a ReadableStream object returns a ReadableStreamDefaultReader object, the reader.

We can get it this way:

(async () => {
  const stream = (await fetch('/')).body
  const reader = stream.getReader()
})()

or more concisely:

(async () => {
  const reader = (await fetch('/')).body.getReader()
})()

We read data in chunks, where a chunk is a byte or a typed array. Chunks are enqueued in the stream, and we read them one chunk at a time.

A single stream can contain different kind of chunks.

Once we have a ReadableStreamDefaultReader object instance we can read data from it using the read() method.

This is how you can read the first chunk of the stream of the HTML content from the web page, byte by byte (for CORS reasons, you can only execute this by opening the Browser DevTools on that web page).

(async () => {
  const reader = (await fetch('/')).body.getReader()
  const { value, done } = await reader.read()
  console.log(value)
})()

The read() method returns an object with 2 properties.

  • done true if the stream ended and we got all the data
  • value the value of the current chunk received

If you open each single group of array items, you’ll get to the single items. Those are bytes, stored in a Uint8Array:

You can transform those bytes to characters using the Encoding API:

(async () => {
  const decoder = new TextDecoder('utf-8')
  const reader = (await fetch('/')).body.getReader()
  const { value, done } = await reader.read()
  console.log(decoder.decode(value))
})()

which will print out the characters loaded in the page:

So as mentioned, the read() method only loads the first chunk of the stream. How can we read all the data in the stream? We must recursively call it.

Here’s an example that loads every chunk of the stream, and prints it:

(async () => {
  const decoder = new TextDecoder('utf-8')
  const reader = (await fetch('/')).body.getReader()

  let charsReceived = 0
  let chunks = []

  const processContent = ({ value, done }) => {
    if (done) {
      console.log('Stream finished. Content received:')
      chunks.forEach(chunk => {
        console.log(decoder.decode(chunk))
      })
      return
    }

    chunks.push(value)
    reader.read().then(processContent)
  }

  reader.read().then(processContent)
})()

I use

reader.read().then(processContent)

as a shorthand for

const { value, done } = await reader.read()
processContent(value, done)

In the next lesson we’ll use Streams at a higher level with WebRTC.

Video