Streams API


Stream interfaces are designed for handling binary data of unspecified length. Large files can be retrieved from the Web as streams and parsed sequentially. Binding a stream to an <audio> or <video> element enables a Web application to start media playing without waiting for the complete load of a media file.

Streams APIs resemble FileReader interfaces in their main functionality. StreamReaders use the same event model as FileReader objects: stream handling is based on the chain of ProgressEvents; the lengthComputable, total and loaded properties of ProgressEvents allow the client-side code to control the flow of a read operation. Here's an example of parsing a large video file as a stream:

StreamReader instantiation
var streamReader=new MSStreamReader();
console.log(streamReader.readyState); // 0: EMPTY

registering event listeners
streamReader.onloadstart=loadstartHandler;
streamReader.onprogress=progressHandler;
streamReader.onabort=abortHandler;
streamReader.onerror=errorHandler;
streamReader.onload=loadHandler;
streamReader.onloadend=loadendHandler;

fetching a file from the Web
var xhr=new XMLHttpRequest();
xhr.onreadystatechange=parseStream;
xhr.open("GET", "video.mp4");
xhr.responseType="ms-stream";
xhr.send();

function parseStream(event) {
 if(event.target.readyState==3) {
  var stream=event.target.response;
  console.info(stream.type);
  streamReader.readAsArrayBuffer(stream);
 }
}

stream reading has begun
function loadstartHandler(event) {
 console.info("StreamReader has started parsing the stream.");
 console.log(event.lengthComputable); // false
 console.log(event.total); // 0: the stream size is still unknown
 console.log(event.loaded); // 0: no bytes are loaded yet
 console.log(event.target.readyState); // 1: LOADING
 console.log(event.target.result); // null
}

partial stream data is parsed
function progressHandler(event) {
 console.info(new Intl.NumberFormat([navigator.language]).format(event.loaded)+" bytes are loaded");
 console.log(event.lengthComputable); // true when the last progress event is fired, false otherwise
 console.log(event.total); // the stream size in bytes when the last progress event is fired, 0 otherwise
 console.log(event.target.readyState); // 1: LOADING
 console.log(event.target.result); // ArrayBuffer
 console.log(event.target.result.byteLength); // the buffer length is incremented progressively
}

StreamReader's abort() method has been called
function abortHandler(event) {
 console.warn("Stream reading was cancelled by the user . . .");
 console.log(event.target.result); // null
}

an error has occurred
function errorHandler(event) {
 console.error(event.target.error);
}

stream reading is finished without errors
function loadHandler(event) {
 console.info("The operation is completed successfully.");
 console.log(event.lengthComputable); // true
 console.log(event.total); // the stream size in bytes
 console.log(event.loaded); // the same value
 console.log(event.target.readyState); // 2: DONE
 console.log(event.target.result); // ArrayBuffer containing all bytes of the stream
 console.log(event.target.result.byteLength); // the buffer length is equal to the stream size
 var blob=new Blob([event.target.result]);
 navigator.msSaveBlob(blob, "video.mp4");
}

marking the end of the read operation
irrespective of its sucess or failure
function loadendHandler(event) {
 console.info("Stream reading is over.");
}

Let's go into details:

  • a video file is requested as a stream by setting the responseType property of the XMLHttpRequest to ms-stream: at present Streams API is only implemented in Internet Explorer, so the code uses a prefixed version of the standard value;
  • the stream is made available when the request's readyState is equal to 3 (LOADING);
  • the response property of the XHR is returned as an instance of MSStream - a prefixed form of the Stream interface; the stream has the type property reflecting the MIME type of the fetched resource (video/mp4);
  • the stream is passed to one of the read methods of a MSStreamReader;
  • the example code attaches a number of event listeners to the MSStreamReader and reads the video stream as an ArrayBuffer; the readyState property of the reader is changing from 0 (EMPTY) to 2 (DONE);
  • the result property of the StreamReader will hold the stream data as a binary buffer in the end; just to materialize the file loading, the example creates a Blob from the buffer and invokes IE-specific navigator.msSaveAsBlob(): Internet Explorer will display a notice prompting the user to save the video file.

StreamReaders have four read methods; three of them are equivalent to the FileReader interface members: these are readAsText(), readAsArrayBuffer() and readAsDataURL(). In addition, StreamReaders can parse streams as Blobs by calling the readAsBlob(). Any method can limit the number of stream bytes to be read. Stream truncating is enabled by supplying an additional argument:

only 2000 bytes of the stream will be read
streamReader.readAsArrayBuffer(stream, 2000);

If a stream is parsed as text, the maximum number of bytes to be read before completion is the third argument of the read method; the second argument is the text encoding:

streamReader.readAsText(stream, "UTF-8", 2000);

A streams can be used to create an object URL. The URL is considered a valid source of a media element:

media element for playing an audio stream
<audio id="music" autoplay controls></audio>

obtaining a stream from HTTP message exchange
var xhr=new XMLHttpRequest();
xhr.onreadystatechange=playAudio;
xhr.onprogress=monitorXHR;
xhr.open("GET", "Tartini.mp3");
xhr.responseType="ms-stream";
xhr.send();

function playAudio(event) {
 if(event.target.readyState==3) {
  var stream=event.target.response; // MSStream object
  console.info(stream.type); // MIME type of the stream
  var url=URL.createObjectURL(stream);
  var audio=document.getElementById("music");
  audio.src=url;
 }
}

function monitorXHR(event) {
 console.log(new Intl.NumberFormat([navigator.language]).format(event.loaded)+" bytes are loaded");
}

Local files can be played as streams, too. One of interesting features of object URLs is that they are allowed to be passed to the XMLHttpRequest open() method like any other conventional URL. In such cases the browser emulates HTTP message exchange and provides responses based on such URLs with Content-Type and Content-Length headers. In the example below, a local audio file is selected by the user and played as a stream. This time the code uses an anonymous function to handle the readyState changes:

input element for selecting a local audio file
<input type="file" name="file-control" accept="audio/*" onchange="playAudioFileAsStream(this.files[0])">

parsing the local file as stream
function playAudioFileAsStream(file) {
 var xhr=new XMLHttpRequest();
 xhr.onreadystatechange=function() { // anonymous handler
  if(this.readyState==this.LOADING) { // XHR is referenced as this
   console.info(this.getAllResponseHeaders());
   var audio=document.getElementById("music");
   audio.src=URL.createObjectURL(this.response);
  }
 };
 xhr.open("GET", URL.createObjectURL(file));
 xhr.responseType="ms-stream";
 xhr.send();
}

No doubt stream interfaces are useful for media-oriented applications, especially when media data is generated dynamically on the server side. Parsing a large file as a stream improves performance and enhances user experience. However, the future of the Streams API depends on its implementation in flagship browsers. Streams are not supported by Firefox and Safari, though the interface is in active development by Chrome and Opera teams. Loading large files in Firefox after the manner of streams can be facilitated by defining the response type of an XMLHttpRequest as moz-chunked-arraybuffer or moz-chunked-text.