WhiteSites Blog

FLV Scrubbing with throttling

Posted on Apr 30, 2010 by Paul White

In the old days of the internet if you wanted to provide videos on your website, you would use embed tags, or play an FLV via a flash SWF.  But the problem with this the huge amount of wasted transfer that results.  So I set out to develop a customized handler using asp.net that would allow FLV scrubbing, and also allow throttling to save on bandwidth.

What is FLV Scrubbing?

Those of you who have been to youtube of any modern porn site have noticed how you can drag the slider and jump to later cue points in the movie without having to load the entire movie.  This is beneficial in two ways.  It allows people to skip the parts of the video they don't want to watch and get right to the climax of the video.  For Websites it allows them to save a few KB to a few MB of transfer since they don't have to send over the boring parts of the video.  So it enhances the user experience as well as saves money on the hosting side.  Its a win win, but providing this kind of interface is no easy task.

What is FLV throttling?

Throttling is the act of artificially limiting the rate of transfer.  In the case of FLV videos, they would normally be downloaded by the client as fast as the network and server's hardware would allow. This might be at a full 100M/bit if the client is on a fast enough connection.  But lets say the client was going a watch a video that was 5 minutes long, and 100 MB in size.  They get through the first 20 seconds and realize they don't like the video so they browse to something else.  However the 100 MB video has already downloaded.  that was probably about 90 MB of transfer wasted.  Instead of letting the client download the entire video as fast as they could what if you throttled the rate of transfer to the actual rate of demand.  There is no reason you should have to feed data to the client faster than they need it.  So if your video plays at 100KB/sec there is no reason to transfer the data faster then that.  Of course there is no reason you should force people to wait for the initial buffering time.  So we will give the client full transfer speed as a buffering turbo, then throttle their transfer rate after they have a good 2-3 second buffer.

FLV scrubbing and throttling with asp.net

There are a few issues we need to overcome before we can effectively do this.  The first issue is FLV files by default do not have cue points.  To create the cue points we need to modify them with a flvtool.  FLVTOOL is an dos executable that will automatically modify the FLV with the proper cue points, so we can scrub the video.  The other issue is in order to effectively throttle the data rate we need to know what they minimum rate of transfer is.  In ASP.NET throttling the transfer rate is nothing more than using a System Pause for X miliseconds between sending over each buffer of data.  But this presents even more problems. Not every client is going to transfer at the same rate.  So your code is going to have to be smart and determine what the optimal pause time is.  Second we need to create a non throttled buffer to ensure people wait as little as possible for the data to buffer up and the video to start playing.

Working Example

Here is my FLVStreaming.cs file.
I just dump this into my App_Code directory, makes some edits to my web.config and it automatically will intercept all requests for FLV files.  Whenever a Request for an FLV is handled by default it will be throttled to 150 KB /sec, after a 3 second full speed buffer.  However if a duration variable is passed in the QueryString of the FLV request, it will use that to determine what the best transfer rate is, and throttle it to that.  If you want to use the Scrubbing feature you will need to impliment a SWF FLV video player that supports it.  I have links at the bottom of the page to a few other sites that include a SWF example.

using System;
using System.Threading;
using System.Diagnostics;
using System.IO;
using System.Web;
using System.Net;
using System.Data;
using System.Data.Odbc;


public class FLVStreaming : IHttpHandler

    private static readonly byte _flvheader = HexToByte("464C5601010000000900000009"); //"FLVx1x1000x9000x9"

    public FLVStreaming()
   
   

    public void ProcessRequest(HttpContext context)
   
        try
       
            int pos;
            int length;

            // Check start parameter if present
            string filename = Path.GetFileName(context.Request.FilePath);
            decimal duration = 0;
            if(context.Request.Params"duration"!=null && context.Request.Params"duration"!="")
                duration=Convert.ToDecimal(context.Request.Params"duration");
           
            decimal datalength=0;

            using (FileStream fs = new FileStream(context.Server.MapPath(filename), FileMode.Open, FileAccess.Read, FileShare.Read))
           
               
                string qs = context.Request.Params"start";

                if (string.IsNullOrEmpty(qs))
               
                    pos = 0;
                    length = Convert.ToInt32(fs.Length);
                    datalength= Convert.ToDecimal(fs.Length);
               
                else
               
                    pos = Convert.ToInt32(qs);
                    length = Convert.ToInt32(fs.Length - pos) + _flvheader.Length;
                    datalength= Convert.ToDecimal(fs.Length);
               

                // Add HTTP header stuff: cache, content type and length       
                context.Response.Cache.SetCacheability(HttpCacheability.Public);
                context.Response.Cache.SetLastModified(DateTime.Now);

                context.Response.AppendHeader("Content-Type", "video/x-flv");
                context.Response.AppendHeader("duration", Convert.ToString(duration));
                context.Response.AppendHeader("Content-Length", length.ToString());

                // Append FLV header when sending partial file
                if (pos > 0)
               
                    context.Response.OutputStream.Write(_flvheader, 0, _flvheader.Length);
                    fs.Position = pos;
               

                // This much data will be sent over at a time
                const int buffersize = 16384; // 16KB buffer
               
                // Calculate the transfer rate required for continous play
                decimal maxTransferRate = 153600; // 150KB/ sec
                if(duration!=null && duration!=0)
                    // if duration was provided then we can calculate this video's required maxTransferRate
                    maxTransferRate=(datalength/duration);
               
               
                // how many buffers need to be flushed / second
                decimal buffersPerSecond = maxTransferRate/buffersize;
               
                // how much time each buffer needs to take
                decimal delayPerBuffer= (1/buffersPerSecond)1000;
               
                // how many initial seconds of video should load at full speed
                decimal secondsToBuffer=3;
               
                // how many initial cycles will be at full speed
                int speedboost=Convert.ToInt32((maxTransferRatesecondsToBuffer)/buffersize);
               
                decimal sleepTime = 0;
                decimal currentTransferRate=0;
               
               
               
                byte buffer = new bytebuffersize;
               
                int count = fs.Read(buffer, 0, buffersize);
                while (count > 0)
               
                    if (context.Response.IsClientConnected)
                   
                       
                        Stopwatch stopWatch = new Stopwatch();
                        // start stop watch
                        stopWatch.Start();
                       
                        // transfer data
                        context.Response.OutputStream.Write(buffer, 0, count);
                        context.Response.Flush();
                        count = fs.Read(buffer, 0, buffersize);
                       
                        // stop stop watch
                        stopWatch.Stop();
                       
                        //calculate transfer rate
                        TimeSpan ts = stopWatch.Elapsed;
                       
                        if(speedboost<0)
                            // determine how long we need to sleep the thread to throttle down the bandwidth
                            sleepTime=delayPerBuffer-Convert.ToDecimal(1000ts.TotalSeconds);
                            // sleep the thread
                            Thread.Sleep(Convert.ToInt32(sleepTime));
                       
                        else
                            speedboost--;
                       
                       
                   
                    else
                   
                        count = -1;
                   
               
               
                 
           
       
        catch (Exception ex)
       
            System.Diagnostics.Debug.WriteLine(ex.ToString());
       
   

    public bool IsReusable
   
        get return true;
   

    private static byte HexToByte(string hexString)
   
        byte returnBytes = new bytehexString.Length / 2;
        for (int i = 0; i < returnBytes.Length; i++)
            returnBytesi = Convert.ToByte(hexString.Substring(i 2, 2), 16);
        return returnBytes;
   
   
     public string ByteArrayToString(byte bytes)
       
            string byteString = string.Empty;
            foreach (byte b in bytes)
           
                byteString += Convert.ToChar(b).ToString();
           
            return byteString;
       
   
      public double GetNextDouble(FileStream fileStream, int offset, int length)
       
            // move the desired number of places in the array
            fileStream.Seek(offset, SeekOrigin.Current);
            // create byte array
            byte bytes = new bytelength;
            // read bytes
            int result = fileStream.Read(bytes, 0, length);
            // convert to double (all flass values are written in reverse order)
            return ByteArrayToDouble(bytes, true);
       
   
        public double ByteArrayToDouble(byte bytes, bool readInReverse)
       
            if (bytes.Length != 8)
                throw new Exception("bytes must be exactly 8 in Length");
            if (readInReverse)
                Array.Reverse(bytes);
            return BitConverter.ToDouble(bytes, 0);
       


You will also need to make sure you add the following into your web.config

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <system.webServer>
        <defaultDocument>
            <files>
                <remove value="Default.htm" />
            </files>
        </defaultDocument>
        <handlers>
            <add name="FLVStreaming" path=".flv" verb="" type="FLVStreaming" resourceType="Unspecified" preCondition="integratedMode" />
        </handlers>
    </system.webServer>
</configuration>

Also not to forget the many sites that had the initial idea, I give them full credit for the scrubbing examples, the addition of the throttling was my own idea.

http://blogs.ugidotnet.org/kfra/archive/2006/10/04/50003.aspx
http://www.topfstedt.de/weblog/?page_id=208


Permalink
12969 Visitors
12969 Views

Categories associated with FLV Scrubbing with throttling

Discussion

Arty | May 7, 2011 4:34 AM
Hello, nice post!
But i just can't make this thing work on iis 7.5 64bit, can u help me?
Arty | May 7, 2011 5:36 AM
I'm already done! 
Thanks for the article!
name
Email Needed to confirm comment, but not made public.
Website
 
 
When you Post your Comment, you'll be sent a confirmation link. Once you click this link your thoughts will be made public.. Posts that are considered spam will be deleted, Please keep your thoughts and links relavent to this Article