Streaming Binary Files in Chunks

Audience Level

Intermediate to advanced

Summary

A JScript ASP function to open a binary file (image, audio, video, etc) and stream it to the client browser without consuming too many webserver resources (specifically memory).

Why?

Conventional file streaming attempts to load the file into server memory first and then uses the default system buffering to write the content to the client. Loading the file into memory could be bad if the file is large and/or the server is busy and/or the server has low memory resources. Streaming files in chunks using the function below means that for large files there's a much reduced load on the server's resources, it also helps prevent timeouts during streaming for those on slow connections.

Instructions

Invoke as doStreamChunkedBinaryFile(strAbsFilePath, strMimeType, intKBChunkSize) where “strAbsFilePath” is the absolute filepath and filename of the file to stream to the client, e.g. “D:/website/images/pilchards.jpg”, “strMimeType” is the appropriate mime-type for the content (e.g. “image/jpeg”) and “intKBChunkSize” is the optional number of bytes in KiloBytes in which to load chunks of the file into memory, and then write them to the client until the end of file.

Functions isNumber() and propError(), etc are defined in the JScript ASP library and cacheControl() is defined separately (see link).

Source Code

/*
Function: doStreamChunkedBinaryFile()
Description: Open a binary file (image, audio, video, etc) and stream it to the client browser without consuming too many webserver resources
Returns: n/a
History:
20070520 0048BST    v1      Andrew Urquhart     Created
*/

function doStreamChunkedBinaryFile(strAbsFilePath, strMimeType, intKBChunkSize) {
    try {
        if (!strAbsFilePath) {
            throw new Error(1, "Expected parameter \"strAbsFilePath\" was undefined");
        }
        if (!strMimeType) {
            throw new Error(2, "Expected parameter \"strMimeType\" was undefined");
        }
        if (!isNumber(intKBChunkSize)) {
            var intKBChunkSize = 16; // Default to 16KB chunks
        }
        
        var intChunkSize = intKBChunkSize * 1024; // Bytes
        


        // TEST IF FILE EXISTS
        var objFSO = new ActiveXObject("Scripting.FileSystemObject");
        if (!objFSO.FileExists(strAbsFilePath)) {
            Response.Clear();
            Response.Write("File Not Found");
            Response.Status = 404;
            Response.End();
        }
        // /TEST IF FILE EXISTS



        // PERFORM HTTP 304 CACHE CONTROL - THIS BLOCK OPTIONAL
        var objFile     = objFSO.GetFile(strAbsFilePath);
        var strFileName = objFile.Name;
        var objDate     = new Date(objFile.DateLastModified);
        objFile = objFSO = null;
        cacheControl(objDate, false);
        // /PERFORM HTTP 304 CACHE CONTROL



        // DOWNLOAD IN SMALLISH CHUNKS TO CONSERVE SERVER MEMORY
        var objStream = Server.CreateObject("ADODB.Stream");
        try {
            Response.Clear();
            Response.Buffer = false; // We're doing our own buffering instead
            Response.ContentType = strMimeType;
            objStream.Open();
            objStream.Type  = adTypeBinary;
            objStream.LoadFromFile(strAbsFilePath);
            Response.AddHeader("Content-Length", objStream.Size);
            Response.AddHeader("Content-Disposition","attachment; filename=\"" + encodeURI(strFileName) + "\"");

            // 3 minutes timeout or, if higher, the estimated download time on a 48Kb/s connection
            Server.ScriptTimeout = Math.max(180,  Math.ceil((objStream.Size * 8) / 48E3));

            // Push 'intChunkSize' chunks to client. When near EOS, Read() will terminate at EOS regardless of being asked to read passed EOS
            while (!objStream.EOS) {
                Response.BinaryWrite(objStream.Read(intChunkSize));
                Response.Flush();
            }
            Response.End();
        }
        finally {
            objStream.Close();
        }
        // /DOWNLOAD
    }
    catch (err) {
        propError(err, arguments);
    }
}

Download

Download the source directly.

Note

Argh. On further inspection of memory resources the “LoadFromFile” method appears to pre-load the entire file to memory, thus chunked reading and writing as in the above example is probably less efficient than just than doing a read-all with “Read()” and “BinaryWrite”'ing the whole thing to the client with ASP's “Response.Buffer = true”. As yet I haven't found a way to partially load a file into memory, and until I do, or you do, there's probably not much point in using the above main code example. You'd probably be much better off writing your own component to do this and having it only read in a couple of packets worth of KB per read request in a loop until EOF.

You need to use this function before you have committed sending headers to the client. I.e. you need to invoke it before any “Response.Write()” statements or before any other text/HTML output.

If you encounter syntax errors with the ADO constants in your ASP pages (i.e. “adTypeBinary”) then you need to add the ADODB Metadata Type library declaration to the top of your “global.asa”, e.g.:

<!--METADATA NAME="Microsoft ActiveX Data Objects 2.5 Library" TYPE="TypeLib" UUID="{00000205-0000-0010-8000-00AA006D2EA4}"-->

Advertisement

Feedback

Voting Panel
Is this useful?
or
Did you find any bugs?
or
Did it solve your programming problem?
or
Rate this script: (0 = poor, 5 = very good)
Answers are anonymous, only the combined totals are stored. Uses cookies.