Table of Contents#
- Understanding the Core Concepts
- MediaSource Extensions (MSE)
- ReadableStream
- Fetch/XHR and Range Requests
- Prerequisites
- Step-by-Step Implementation
-
- Set Up the HTML Structure
-
- Initialize MediaSource and Attach to Video
-
- Fetch the Video and Stream the Response
-
- Process the Stream and Append Chunks to MSE
-
- Avoid Duplicate Requests: The Key Advantage
-
- Key Considerations
- CORS and Server Configuration
- Codec and MIME Type Support
- Error Handling and Buffer Management
- Troubleshooting Common Issues
- Conclusion
- 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 aReadableStreamDefaultReader, which we use to read chunks withreader.read().reader.read()returns a promise resolving to{ done: boolean, value: Uint8Array }, wherevalueis the next chunk of data.
Appending Chunks to MSE#
sourceBuffer.appendBuffer(value)adds the chunk to MSE’s buffer.- We must wait for the
updateendevent before appending the next chunk ifsourceBuffer.updatingistrue(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
BlobURLs withfetch(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
fetchrequest to stream the video. - Data is processed incrementally via
ReadableStreamand 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: bytesCodec 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.,
fetchrejects). - 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
mimeCodecstring. - Fix: Use
ffprobeor mp4box.js to get the exact codec (e.g.,avc1.42E01Efor H.264 Baseline 3.0).
Video Doesn’t Play Until Fully Downloaded#
- Issue: Server doesn’t support range requests, or
fetchis reading the entire response first. - Fix: Ensure the server returns
Accept-Ranges: bytes. Test withcurl -r 0-100 https://your-server.com/video.mp4to verify range requests work.
CORS Errors#
- Issue: Missing
Access-Control-Allow-Originheader. - 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.
ReadableStreamprocesses data incrementally, avoiding full downloads.- A single
fetchrequest eliminates duplicate network calls.
For advanced use cases (e.g., adaptive bitrate streaming), extend this with range requests and chunk prioritization.