IPCamAppBar/CameraMJPEGStream.cs

111 lines
3.9 KiB
C#

using System;
using System.Drawing;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace IPCamAppBar
{
internal class CameraMJPEGStream : CameraStream
{
// MJPEG decoding is a variation on https://github.com/arndre/MjpegDecoder
private static readonly byte[] JpegSOI = { 0xff, 0xd8 }; // start of image bytes
private static readonly byte[] JpegEOI = { 0xff, 0xd9 }; // end of image bytes
private const int ChunkSize = 1024;
private const int MaxBufferSize = 1024 * 1024 * 10;
protected override async Task ReadFrames(Stream stream, CancellationToken cancellationToken)
{
var buffer = new byte[ChunkSize];
var bufferPosition = 0;
int? startOfImage = null;
int? lastEndOfSearch = null;
void ExpandBuffer()
{
// Make sure we have at least ChunkSize remaining
if (buffer.Length >= bufferPosition + ChunkSize)
return;
// If we pass the MaxBufferSize (10mb unless changed above), this is likely not an MJPEG stream, abort
if (bufferPosition + ChunkSize > MaxBufferSize)
throw new IOException("Buffer size exceeded before encountering JPEG image");
Array.Resize(ref buffer, bufferPosition + ChunkSize);
}
void ResetBuffer(int untilPosition)
{
// Don't resize the buffer down, it is very likely the next image is of a similar size.
// Instead move whatever's remaining to the start.
if (untilPosition < buffer.Length - 1)
Array.Copy(buffer, untilPosition, buffer, 0, bufferPosition - untilPosition);
bufferPosition = 0;
startOfImage = null;
lastEndOfSearch = null;
}
while (!cancellationToken.IsCancellationRequested)
{
var bytesRead = await stream.ReadAsync(buffer, bufferPosition, ChunkSize, cancellationToken);
if (bytesRead == 0)
throw new EndOfStreamException();
bufferPosition += bytesRead;
if (!startOfImage.HasValue)
{
var index = buffer.Find(JpegSOI, bufferPosition);
if (index == -1)
{
// No start of image yet, we need to buffer more
ExpandBuffer();
continue;
}
startOfImage = index;
}
var endOfImage = buffer.Find(JpegEOI, bufferPosition, lastEndOfSearch.GetValueOrDefault(startOfImage.Value));
if (endOfImage == -1)
{
// No start of image yet, we need to buffer more. Keep track of where we were so we don't
// need to scan it all again
lastEndOfSearch = bufferPosition - JpegEOI.Length;
ExpandBuffer();
continue;
}
if (endOfImage < startOfImage.Value)
{
// Oops, wut?! Uhm. yeah. let's pretend this never happened, ok?
ResetBuffer(startOfImage.Value + JpegSOI.Length);
continue;
}
endOfImage += JpegEOI.Length;
HandleFrame(buffer, startOfImage.Value, endOfImage);
ResetBuffer(endOfImage);
}
}
protected void HandleFrame(byte[] buffer, int start, int end)
{
using (var image = new Bitmap(new MemoryStream(buffer, start, end - start)))
{
OnFrame(new FrameEventArgs
{
Image = image
});
}
}
}
}