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 useawait
.
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
(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 datavalue
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.