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.