I’m trying to do a javascript fetch to grab a video file using fetch. I am able to get the file downloaded and get the blob URL, but I can’t seem to get the progress while its downloading.
I tried this:
let response = await fetch('test.mp4'); const reader = response.body.getReader(); const contentLength=response.headers.get('Content-Length'); let receivedLength = 0; d=document.getElementById('progress_bar'); while(true) { const {done, value} = await reader.read(); if (done) { break; } receivedLength += value.length; d.innerHTML="Bytes loaded:"+receivedLength; } const blob = await response.blob(); var vid=URL.createObjectURL(blob);
The problem is that I get “Response.blob: Body has already been consumed”. I see that the reader.read() is probably doing that. How do I just get the amount of data received and then get a blob URL at the end of it?
Thanks.
Advertisement
Answer
Update:
My first attempt collected the chunks as they downloaded and them put them back together, with a large (2-3x the size of the video) memory footprint. Using a ReadableStream has a much lower memory footprint (memory usage hovers around 150MB for a 1.1GB mkv). Code largely adapted from the snippet here with only minimal modifications from me:
<div id="progress_bar"></div> <video id="video_player"></video>
const elProgress = document.getElementById('progress_bar'), player = document.getElementById('video_player'); function getVideo2() { let contentType = 'video/mp4'; fetch('$pathToVideo.mp4') .then(response => { const contentEncoding = response.headers.get('content-encoding'); const contentLength = response.headers.get(contentEncoding ? 'x-file-size' : 'content-length'); contentType = response.headers.get('content-type') || contentType; if (contentLength === null) { throw Error('Response size header unavailable'); } const total = parseInt(contentLength, 10); let loaded = 0; return new Response( new ReadableStream({ start(controller) { const reader = response.body.getReader(); read(); function read() { reader.read().then(({done, value}) => { if (done) { controller.close(); return; } loaded += value.byteLength; progress({loaded, total}) controller.enqueue(value); read(); }).catch(error => { console.error(error); controller.error(error) }) } } }) ); }) .then(response => response.blob()) .then(blob => { let vid = URL.createObjectURL(blob); player.style.display = 'block'; player.type = contentType; player.src = vid; elProgress.innerHTML += "<br /> Press play!"; }) .catch(error => { console.error(error); }) } function progress({loaded, total}) { elProgress.innerHTML = Math.round(loaded / total * 100) + '%'; }
First Attempt (worse, suitable for smaller files)
My original approach. For a 1.1GB mkv, the memory usage creeps up to 1.3GB while the file is downloading, then spikes to about 3.5Gb when the chunks are being combined. Once the video starts playing, the tab’s memory usage goes back down to ~200MB but Chrome’s overall usage stays over 1GB.
Instead of calling response.blob()
to get the blob, you can construct the blob yourself by accumulating each chunk of the video (value
). Adapted from the exmaple here: https://javascript.info/fetch-progress#0d0g7tutne
//... receivedLength += value.length; chunks.push(value); //... // ==> put the chunks into a Uint8Array that the Blob constructor can use let Uint8Chunks = new Uint8Array(receivedLength), position = 0; for (let chunk of chunks) { Uint8Chunks.set(chunk, position); position += chunk.length; } // ==> you may want to get the mimetype from the content-type header const blob = new Blob([Uint8Chunks], {type: 'video/mp4'})