Chunking large video file with webservice

Introduction

This blog gives an idea of uploading a large video file in chunks, and encrypting chunks as they are received on the server.

Requirements

Following are the key requirements

  • Functionality to upload large video files using a webservice
  • Instead of sending file in a single go, client should divide the file into chunks
  • Server should encrypt every chunk received and save

Of-course we can propose WCF to upload the data either by using streaming or chunking efficiently, but there might be cases where we need to extend an existing webservice (ASMX) to allow uploading large video files and secure them with minimal changes in design/code. 

Considering the fact that video files can be huge, uploading them in a single go is not a preferred approach because of high memory usages while buffering, and yes, there is a limit of the file upload size. While uploading the chunks, we also have to ensure that every chunk is saved securely (encrypted) on the server. Later this file can be sent decrypted to trusted client(s).

Implementation

We need a symmetric key (or private-key) which uses a single key to encrypt and decrypt. The RijndaelManaged class under System.Security.Cryptography namespace can be used to create symmetric key with the CryptoStream to encrypt the byte array. 

For demonstration purpose, let's build a simple win form which will send chunks of a video file, and a web service which will receive these chunks and will encrypt them while saving to the disk. In order to use the RijndaelManaged class, we can either use the default Key and IV (Initialize vector) provided by class itself, or we can use our own password to generate Key and IV.

Following code can be used to generate encryption Key and IV based on a password string in the webservice

 /// <summary>/// Password string/// </summary>const string password = "some_password";       /// <summary>/// Generates encryption Key based on the password/// </summary>/// <param name="keySize"></param>/// <returns></returns>public static byte[] GetEncryptionKey(int keySize){    Rfc2898DeriveBytes key = new Rfc2898DeriveBytes(password,              Encoding.ASCII.GetBytes(password.ToCharArray()));    return key.GetBytes(keySize / 8);           }/// <summary>/// Generates encryption IV based on the password/// </summary>/// <param name="blockSize"></param>/// <returns></returns>public static byte[] GetEncryptionIV(int blockSize){    Rfc2898DeriveBytes key = new Rfc2898DeriveBytes(password,                    Encoding.ASCII.GetBytes(password.ToCharArray()));    return key.GetBytes(blockSize / 8);}

Okay, let's use above Key and IV and write the webmethod which will receive chunks and will encrypt them. The webmethod has only one parameter of type UploadRequest. The implementaiton of UploadRequest class can be found in the sample code.

 /// <summary>/// Receives chunks of video file, encrypt them and save to disk/// </summary>/// <param name="objRequest">Instance of class UploadRequest</param>[WebMethod]        public void UploadVideo(UploadRequest objRequest){    long length = 0;    string tempUploadLocation = string.Format("C:\\temp\\{0}",  Path.GetFileName(objRequest.FileName));    if (objRequest.FileBytes.Length > 0)    {        using (FileStream writer = new FileStream(tempUploadLocation,  File.Exists(tempUploadLocation) ? FileMode.Append :  FileMode.Create, FileAccess.Write))        {            RijndaelManaged rm = new RijndaelManaged();            using (CryptoStream crStream = new CryptoStream(writer,                       rm.CreateEncryptor(EncryptionHelper.GetEncryptionKey(rm.KeySize),                                  EncryptionHelper.GetEncryptionIV(rm.BlockSize)), CryptoStreamMode.Write))            {                crStream.Write(objRequest.FileBytes, 0, objRequest.FileBytes.Length);                length = writer.Length;            }        }    }    //// Check if file upload is complete    if (length >= objRequest.FileSize)    {        //// Post upload processing goes here     }           }

To decrypt the video, we need same symmetric key. Following code can be used to decrypt the video 

 /// <summary>/// Decrypt the file and save it to the temp location (Server)/// </summary>/// <param name="filePath"></param>[WebMethod]public string DecryptVideoAndSaveInTemp(string filePath){    byte[] bytMedia = null;    string tempDownloadLocation = string.Format("D:\\Temp\\{0}",Path.GetFileName(filePath));    using (FileStream fs1 = File.Open(filePath, FileMode.Open, FileAccess.Read))    {                        RijndaelManaged rm = new RijndaelManaged();        rm.Padding = PaddingMode.None;        //// Decrypt the file        using (CryptoStream cs = new CryptoStream(fs1, rm.CreateDecryptor(EncryptionHelper.GetEncryptionKey(rm.KeySize), EncryptionHelper.GetEncryptionIV(rm.BlockSize)), CryptoStreamMode.Read))        {            bytMedia = new byte[fs1.Length];            Stream streamObject = cs;            streamObject.Read(bytMedia, 0, (int)fs1.Length);        }        //// Use the decrypted byte array and save to the disk        using (FileStream fs = new FileStream(tempDownloadLocation, FileMode.CreateNew))        {            fs.Write(bytMedia, 0, bytMedia.Length);        }                    }    return string.Format("File decrypted and copied to {0}", tempDownloadLocation);}

By now our webservice is ready to receive chunks of the file, now we need a client which will actually divide the video into chunks, and will call the UploadVideo webmethod for each chunk.

The chunkSize (set to 500KB) can be a key factor to improve performance of the upload based upon the factors like network speed, server resources etc. 

 /// <summary>/// Chunk the file and upload/// </summary>/// <param name="filename"></param>private void UploadVideo(string filename){    #region Vars    const int chunkSize = 512000;    byte[] bytes = null;    int startIndex, endIndex, length, totalChunks;               WS.UploadRequest objRequest = new WS.UploadRequest();                #endregion    try    {        if (File.Exists(filename))        {            using (WS.UploadService objService = new WS.UploadService())            {                using (FileStream fs = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read))                {                    //// Calculate total chunks to be sent to service                    totalChunks = (int)Math.Ceiling((double)fs.Length / chunkSize);                    //// Set up Upload request object                    objRequest.FileName = filename;                    objRequest.FileSize = fs.Length;                                                for (int i = 0; i < totalChunks; i++)                    {                        startIndex = i * chunkSize;                        endIndex = (int)(startIndex + chunkSize > fs.Length ? fs.Length : startIndex + chunkSize);                        length = endIndex - startIndex;                        bytes = new byte[length];                        //// Read bytes from file, and send upload request                        fs.Read(bytes, 0, bytes.Length);                        objRequest.FileBytes = bytes;                        objService.UploadVideo(objRequest);                    }                }            }        }    }    catch (Exception ex)    {        MessageBox.Show(string.Format("Error- {0}", ex.Message));    }}

 

Is it limited to video files only?

No, this technique is not limited to video files, but the sample code is not tested with other file types.

 

The sample code can be downloaded from here .