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.
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'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's protect _Position: _Position might be changed by another Read() or Seek()
lock (_Lock)
{
int TotalReadBytes = 0;
int RestBytesToCopy = cb;
int OffsetInOutput = 0;
// Let'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's find the chunk number that corresponds
// with current position
long RequiredChunkIndex = _Position / Program.ChunkSize;
// We do cache decrypted data, so let's update the cache if it'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'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.