cyberangles blog

How to Play a Video While Downloading in Chrome: Using Fetch/XHR, ReadableStream, and MediaSource Without Duplicate Requests

In today’s digital world, users expect seamless video playback—even when content is still downloading. Traditional approaches often force users to wait for an entire video file to download before playback starts, or worse, trigger duplicate network requests (e.g., re-fetching the same video file when the user seeks or the browser reloads). This wastes bandwidth, degrades user experience, and increases load times.

In this guide, we’ll explore a modern solution to stream and play video while downloading in Chrome, using three powerful web APIs:

  • Fetch/XHR: To initiate a single network request for the video file.
  • ReadableStream: To process the video data in chunks as it arrives, without waiting for the entire file.
  • MediaSource Extensions (MSE): To dynamically feed these chunks into the browser’s media pipeline for immediate playback.

By combining these tools, we avoid duplicate requests, reduce latency, and enable smooth progressive playback. Let’s dive in!

2026-02

Table of Contents#

  1. Understanding the Core Concepts
    • MediaSource Extensions (MSE)
    • ReadableStream
    • Fetch/XHR and Range Requests
  2. Prerequisites
  3. Step-by-Step Implementation
      1. Set Up the HTML Structure
      1. Initialize MediaSource and Attach to Video
      1. Fetch the Video and Stream the Response
      1. Process the Stream and Append Chunks to MSE
      1. Avoid Duplicate Requests: The Key Advantage
  4. Key Considerations
    • CORS and Server Configuration
    • Codec and MIME Type Support
    • Error Handling and Buffer Management
  5. Troubleshooting Common Issues
  6. Conclusion
  7. References

Understanding the Core Concepts#

Before diving into code, let’s clarify the tools we’ll use:

MediaSource Extensions (MSE)#

MSE is a JavaScript API that lets you dynamically construct media streams for playback. Instead of pointing a <video> element directly to a file URL (e.g., <video src="video.mp4">), MSE allows you to feed raw media data (chunks) into a buffer, which the browser decodes and plays in real time. This is the foundation for adaptive streaming (e.g., HLS, DASH) and progressive playback.

ReadableStream#

Part of the Streams API, ReadableStream lets you process data as it arrives from a network request (or other sources), rather than waiting for the entire response. This is critical for streaming: we can read video data in chunks and pass them to MSE immediately, enabling playback while the download continues.

Fetch/XHR and Range Requests#

To fetch the video file, we’ll use fetch() (or XMLHttpRequest for older browsers, though fetch is preferred). A key detail: modern servers support range requests (via the Range HTTP header), allowing clients to request specific byte ranges of a file (e.g., "bytes=0-1000"). This is optional but improves efficiency (e.g., resuming interrupted downloads). However, our primary goal here is to avoid duplicates, so we’ll use a single fetch request and stream its response.

Prerequisites#

To follow along, ensure you have:

  • Basic knowledge of HTML, JavaScript, and HTTP.
  • Chrome 52+ (MSE and ReadableStream support is stable here).
  • A video file (MP4 recommended; H.264/AAC codecs work best for broad compatibility).
  • A web server (local or remote) that:
    • Serves the video with proper CORS headers (if testing cross-origin).
    • Supports Accept-Ranges: bytes (optional but recommended for range requests).

Step-by-Step Implementation#

Let’s build a working example. We’ll create a video player that streams and plays a video while downloading, with no duplicate requests.

1. Set Up the HTML Structure#

First, add a <video> element to your page. We’ll control it via JavaScript:

<!DOCTYPE html>
<html>
<body>
  <h1>Stream Video While Downloading</h1>
  <video id="videoPlayer" controls width="800"></video>
  <script src="stream-player.js"></script>
</body>
</html>

2. Initialize MediaSource and Attach to Video#

In stream-player.js, we’ll create a MediaSource object and link it to the <video> element. The MediaSource acts as a "virtual" source for the video.

const video = document.getElementById('videoPlayer');
const mediaSource = new MediaSource();
 
// Attach MediaSource to the video element
video.src = URL.createObjectURL(mediaSource);

3. Fetch the Video and Stream the Response#

Next, we’ll use fetch() to request the video file. Instead of waiting for the full response, we’ll access its body as a ReadableStream and process chunks incrementally.

// Wait for MediaSource to be ready (sourceopen event)
mediaSource.addEventListener('sourceopen', async () => {
  // Step 3.1: Define the video codec (critical for MSE)
  const mimeCodec = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"'; 
  // ^ Replace with your video's codec! Use mp4box.js to detect (see References).
 
  // Step 3.2: Create a source buffer for MSE
  const sourceBuffer = mediaSource.addSourceBuffer(mimeCodec);
 
  try {
    // Step 3.3: Fetch the video and stream the response
    const response = await fetch('video.mp4'); // Replace with your video URL
    const reader = response.body.getReader(); // Get a stream reader
 
    // Step 3.4: Read chunks and append to MSE buffer
    while (true) {
      const { done, value } = await reader.read(); // Read next chunk
 
      if (done) {
        // All chunks processed: signal end of stream
        mediaSource.endOfStream();
        break;
      }
 
      // Append chunk to MSE buffer (wait for buffer to be ready)
      if (!sourceBuffer.updating) {
        sourceBuffer.appendBuffer(value);
      } else {
        // If buffer is busy, wait for 'updateend' before appending
        await new Promise(resolve => {
          sourceBuffer.addEventListener('updateend', resolve, { once: true });
        });
        sourceBuffer.appendBuffer(value);
      }
    }
  } catch (error) {
    console.error('Stream failed:', error);
    video.src = ''; // Clear video source on error
  }
});

4. Key Code Explanations#

Codec String (mimeCodec)#

The mimeCodec string (e.g., 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"') tells MSE the format of the media data. It must match the video’s actual codecs. To find your video’s codecs:

  • Use ffprobe (command-line tool): ffprobe -v error -show_entries stream=codec_name,codec_long_name -of default=noprint_wrappers=1:nokey=1 video.mp4.
  • Or use mp4box.js (browser-based parser).

Streaming with ReadableStream#

  • response.body.getReader() returns a ReadableStreamDefaultReader, which we use to read chunks with reader.read().
  • reader.read() returns a promise resolving to { done: boolean, value: Uint8Array }, where value is the next chunk of data.

Appending Chunks to MSE#

  • sourceBuffer.appendBuffer(value) adds the chunk to MSE’s buffer.
  • We must wait for the updateend event before appending the next chunk if sourceBuffer.updating is true (to avoid overflowing the buffer).

5. Avoid Duplicate Requests: The Key Advantage#

Traditional methods often trigger duplicate requests. For example:

  • If you set <video src="video.mp4">, the browser may fetch the entire file first, then play it. If the user pauses/resumes, it might re-fetch.
  • Using Blob URLs with fetch (e.g., response.blob().then(blob => video.src = URL.createObjectURL(blob))) requires waiting for the entire file to download, then triggers a second request when the blob is read.

In our approach:

  • We use one fetch request to stream the video.
  • Data is processed incrementally via ReadableStream and fed directly to MSE.
  • The <video> element never directly requests the file URL—it’s fed via MSE, so no duplicate network calls.

Key Considerations#

CORS#

If your video is hosted on a different domain, the server must return CORS headers (e.g., Access-Control-Allow-Origin: *). Without this, fetch will block the response, and MSE will fail.

Server Support for Range Requests#

Ensure your server returns Accept-Ranges: bytes in responses. This is enabled by default on most modern servers (Apache, Nginx, S3). To test:

curl -I https://your-server.com/video.mp4
# Look for: Accept-Ranges: bytes

Codec Support#

MSE only works with codecs supported by the browser. For MP4, H.264 (video) and AAC (audio) are universal. Avoid rare codecs (e.g., VP9 in MP4) unless you’re targeting specific browsers.

Error Handling#

Add robust error handling for:

  • Network failures (e.g., fetch rejects).
  • Invalid codecs (MSE throws "No compatible source" errors).
  • Buffer underflow (e.g., chunks arrive slower than playback, causing stalls).

Chunk Size and Memory#

Chunks from ReadableStream are typically ~16KB–64KB. Avoid accumulating chunks in memory—stream them to MSE immediately.

Troubleshooting#

"No Compatible Source Was Found"#

  • Issue: Incorrect mimeCodec string.
  • Fix: Use ffprobe or mp4box.js to get the exact codec (e.g., avc1.42E01E for H.264 Baseline 3.0).

Video Doesn’t Play Until Fully Downloaded#

  • Issue: Server doesn’t support range requests, or fetch is reading the entire response first.
  • Fix: Ensure the server returns Accept-Ranges: bytes. Test with curl -r 0-100 https://your-server.com/video.mp4 to verify range requests work.

CORS Errors#

  • Issue: Missing Access-Control-Allow-Origin header.
  • Fix: Configure your server to include CORS headers. For Nginx, add:
    location /video.mp4 {
      add_header Access-Control-Allow-Origin *;
    }

"MediaSource is Not Defined"#

  • Issue: Browser doesn’t support MSE (rare in Chrome 52+).
  • Fix: Check support with if ('MediaSource' in window) { /* proceed */ }.

Conclusion#

By combining fetch, ReadableStream, and MediaSource Extensions, we’ve built a video player that streams and plays content while downloading—with no duplicate requests. This approach is efficient, reduces bandwidth waste, and improves user experience.

Key takeaways:

  • MSE enables dynamic media buffering for real-time playback.
  • ReadableStream processes data incrementally, avoiding full downloads.
  • A single fetch request eliminates duplicate network calls.

For advanced use cases (e.g., adaptive bitrate streaming), extend this with range requests and chunk prioritization.

References#