Help us improve Softanics
We use analytics cookies to understand which pages and downloads are useful. No ads. Privacy Policy

Play Encrypted Video from Stream (No Disk Writes)

Out of the box, WPF MediaElement and Windows Media Player ActiveX cannot play directly from a .NET Stream. When the source is encrypted, naive solutions decode to a temp file, exposing content on disk. With BoxedApp SDK, you can mount a virtual file backed by a custom COM IStream, so the player gets a normal file path while reads and seeks are transparently routed to your stream. No physical file is created.

The virtual file exists only inside the virtualization layer; it's visible to the current process and child processes that inherit the same virtual environment.

Problem

MediaElement/WMP require a file path and won't accept arbitrary streams for playback. Decoding encrypted content to a temporary file defeats the purpose (data lands on disk and can be extracted).

Solution Overview

Create a BoxedApp virtual file whose backend is your IStream implementation. The player sees a regular file path, while your stream performs lazy decryption: only the requested chunks are decrypted on demand. This scales to large videos and avoids memory bloat and disk I/O.

Step 1 - Encrypt in Chunks

Split the original video into chunks, encrypt each chunk independently, and persist per-chunk metadata (original size, encrypted size, offsets). This allows precise mapping from a virtual file position to the corresponding encrypted range.

static class Program
{
    // To generate these arrays, please use the KeyGen application included in this sample
    public static byte[] Key = { 0x9b, 0x67, 0x14, 0xc, 0xb8, 0x7e, 0xf0, 0x4b, 0x6e, 0xd, 0x88, 0x7a, 0xf1, 0xbb, 0x33, 0xc1, 0xc1, 0x12, 0xa3, 0x1f, 0xca, 0x2d, 0xdc, 0x54 };
    public static byte[] IV = { 0x3d, 0x12, 0xe9, 0x8c, 0xea, 0x24, 0x61, 0xf0 };
 
    // As it's impossible to move the pointer of CryptoStream to any position, we split the input data into chunks
    // and encrypt each chunk
    public static int ChunkSize = 1024 * 1024;
 
    public static byte[] Encrypt(byte[] input, int size)
    {
        byte[] output;
 
        TripleDESCryptoServiceProvider TDES = new TripleDESCryptoServiceProvider();
 
        using (MemoryStream EncryptedDataStream = new MemoryStream())
        {
            using (CryptoStream CryptoStream = new CryptoStream(EncryptedDataStream, TDES.CreateEncryptor(Program.Key, Program.IV), CryptoStreamMode.Write))
            {
                CryptoStream.Write(input, 0, size);
                CryptoStream.FlushFinalBlock();
 
                output = new byte[EncryptedDataStream.Length];
                EncryptedDataStream.Position = 0;
                EncryptedDataStream.Read(output, 0, output.Length);
            }
        }
 
        return output;
    }
}
using (BinaryWriter Writer = new BinaryWriter(_OutputStream))
{ 
    byte[] Buf = new byte[Program.ChunkSize];
 
    List<int> SourceChunkSizeList = new List<int>();
    List<int> EncryptedChunkSizeList = new List<int>();
 
    int ReadBytes;
    long nTotalReadBytes = 0;
    while ((ReadBytes = _InputStream.Read(Buf, 0, Buf.Length)) > 0)
    {
        if (backgroundWorkerEncryption.CancellationPending)
            break;
 
        byte[] EncryptedData = Program.Encrypt(Buf, ReadBytes);
        _OutputStream.Write(EncryptedData, 0, EncryptedData.Length);
 
        SourceChunkSizeList.Add(ReadBytes);
        EncryptedChunkSizeList.Add(EncryptedData.Length);
 
        nTotalReadBytes += ReadBytes;
        backgroundWorkerEncryption.ReportProgress((int)(nTotalReadBytes * 100 / _InputStream.Length));
    }
 
    foreach (int SourceChunkSize in SourceChunkSizeList)
    {
        Writer.Write(SourceChunkSize);
    }
 
    foreach (int EncryptedChunkSize in EncryptedChunkSizeList)
    {
        Writer.Write(EncryptedChunkSize);
    }
 
    Writer.Write((int)EncryptedChunkSizeList.Count);
}

Step 2 - Implement COM IStream

Implement System.Runtime.InteropServices.ComTypes.IStream: provide Read, Seek, and Clone. Maintain current position in the decrypted coordinate space; on Read, locate the chunk(s), decrypt on demand, and return bytes. Consider caching hot chunks to minimize repeated decryptions.

using System.Runtime.InteropServices.ComTypes;
...
namespace Sample8_PlayEncryptedVideo
{
    class VirtualFileStream : IStream
    {
        private string _EncryptedVideoFilePath;
        private Stream _EncryptedVideoFile;
        private int[] _EncryptedChunkLength;
        private long[] _EncryptedChunkPosition;
        private int[] _SourceChunkLength;
        private int _ChunkCount;
        private byte[] _CurrentChunk = new byte[Program.ChunkSize];
        private long _CurrentChunkIndex = -1;
        private long _Position = 0;
        private long _Length;
        private Object _Lock = new Object();
public VirtualFileStream(string EncryptedVideoFilePath)
{
    _EncryptedVideoFilePath = EncryptedVideoFilePath;
    _EncryptedVideoFile = File.Open(EncryptedVideoFilePath, FileMode.Open, FileAccess.Read, FileShare.Read);
 
    // Read chunk data
    using (Stream EncryptedVideoFileStream = File.Open(EncryptedVideoFilePath, FileMode.Open, FileAccess.Read, FileShare.Read))
    {
        using (BinaryReader Reader = new BinaryReader(EncryptedVideoFileStream))
        {
            _Length = 0;
 
            EncryptedVideoFileStream.Position = _EncryptedVideoFile.Length - sizeof(int);
            _ChunkCount = Reader.ReadInt32();
 
            EncryptedVideoFileStream.Position = _EncryptedVideoFile.Length - sizeof(int) - _ChunkCount * sizeof(int) - _ChunkCount * sizeof(int);
 
            _EncryptedChunkLength = new int[_ChunkCount];
            _SourceChunkLength = new int[_ChunkCount];
            _EncryptedChunkPosition = new long[_ChunkCount];
 
            for (int i = 0; i < _ChunkCount; i++)
            {
                _SourceChunkLength[i] = Reader.ReadInt32();
                _Length += _SourceChunkLength[i];
            }
 
            long Offset = 0;
 
            for (int i = 0; i < _ChunkCount; i++)
            {
                _EncryptedChunkLength[i] = Reader.ReadInt32();
                _EncryptedChunkPosition[i] = Offset;
 
                Offset += _EncryptedChunkLength[i];
            }
        }
    }
}
{
    SeekOrigin Origin = (SeekOrigin)dwOrigin;
 
    // Let&apos;s protect _Position: _Position might be changed by Read()
    lock (_Lock)
    {
        switch (Origin)
        {
            case SeekOrigin.Begin:
                {
                    _Position = dlibMove;
                    break;
                }
            case SeekOrigin.Current:
                {
                    _Position += dlibMove;
                    break;
                }
            case SeekOrigin.End:
                {
                    _Position = _Length + dlibMove;
                    break;
                }
        }
    }
 
    if (IntPtr.Zero != plibNewPosition)
        Marshal.WriteInt64(plibNewPosition, _Position);
}
public void Read(byte[] pv, int cb, IntPtr pcbRead)
{
    int ReadBytes;
 
    if (_Position < 0 || _Position > _Length)
    {
        ReadBytes = 0;
    }
    else
    {
        // Let&apos;s protect _Position: _Position might be changed by another Read() or Seek()
        lock (_Lock)
        {
            int TotalReadBytes = 0;
            int RestBytesToCopy = cb;
 
            int OffsetInOutput = 0;
 
            // Let&apos;s move chunk by chunk until all requested data is read or end of file is reached
            while (RestBytesToCopy > 0 && _Position < _Length)
            { 
                // Original data is split into chunks, so let&apos;s find the chunk number that corresponds
                // with current position
                long RequiredChunkIndex = _Position / Program.ChunkSize;
 
                // We do cache decrypted data, so let&apos;s update the cache if it&apos;s not initialized
                // or the cached chunk has another index
                if (-1 == _CurrentChunkIndex || _CurrentChunkIndex != RequiredChunkIndex)
                {
                    _CurrentChunkIndex = RequiredChunkIndex;
 
                    _EncryptedVideoFile.Position = _EncryptedChunkPosition[_CurrentChunkIndex];
 
                    byte[] data = new byte[_EncryptedChunkLength[_CurrentChunkIndex]];
                    _EncryptedVideoFile.Read(data, 0, data.Length);
 
                    _CurrentChunk = Program.Decrypt(data, data.Length);
                }
 
                // So far, we have the decrypted data available, now let&apos;s get the starting point within the chunk
                // and find out how many bytes we can read from the chunk (chunks may have different lengths)
                int OffsetInChunk = (int)(_Position - (_CurrentChunkIndex * Program.ChunkSize));
                int RestInChunk = (int)(_SourceChunkLength[_CurrentChunkIndex] - OffsetInChunk);
 
                int BytesToCopy;
                if (RestInChunk < RestBytesToCopy)
                    BytesToCopy = RestInChunk;
                else
                    BytesToCopy = RestBytesToCopy;
 
                // Copy the data...
                Array.Copy(_CurrentChunk, OffsetInChunk, pv, OffsetInOutput, BytesToCopy);
 
                // ...and move forward
                RestBytesToCopy -= BytesToCopy;
                TotalReadBytes += BytesToCopy;
                OffsetInOutput += BytesToCopy;
                _Position += BytesToCopy;
            }
 
            ReadBytes = TotalReadBytes;
        }
    }
 
    if (IntPtr.Zero != pcbRead)
        Marshal.WriteIntPtr(pcbRead, new IntPtr(ReadBytes));
}

Step 3 - Mount Virtual File and Play

Create a BoxedApp virtual file backed by your IStream and pass its path to the player (MediaElement/WMP). The player performs normal file I/O; your stream handles decryption and seeking.

private void buttonPlay_Click(object sender, EventArgs e)
{
    OpenFileDialog dlg = new OpenFileDialog();
 
    if (dlg.ShowDialog() == DialogResult.OK)
    {
        using (SafeFileHandle fileHandle =
            new SafeFileHandle(
                BoxedAppSDK.NativeMethods.BoxedAppSDK_CreateVirtualFileBasedOnIStream(
                    @"1.avi", // name of the pseudo file
                    BoxedAppSDK.NativeMethods.EFileAccess.GenericWrite,
                    BoxedAppSDK.NativeMethods.EFileShare.Read,
                    IntPtr.Zero,
                    BoxedAppSDK.NativeMethods.ECreationDisposition.New,
                    BoxedAppSDK.NativeMethods.EFileAttributes.Normal,
                    IntPtr.Zero,
                    new VirtualFileStream(dlg.FileName)
                ),
                true
            )
        )
        {
            // We use "using" to close the allocated handle
            // The virtual file will still exist
        }
 
        axWindowsMediaPlayer1.URL = @"1.avi";
    }
}

When to Use

DRM-like playback, premium content protection, kiosks/offline players, and zero-trace scenarios where temporary files are unacceptable.