152 lines
5.4 KiB
C#
152 lines
5.4 KiB
C#
using System;
|
|
using System.Drawing;
|
|
using System.IO;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using IPCamLib.Base;
|
|
|
|
// ReSharper disable UnusedMember.Global - public API
|
|
|
|
namespace IPCamLib.Concrete
|
|
{
|
|
/// <summary>
|
|
/// Implements the ICamera interface for IP cameras exposing an MJPEG stream over HTTP.
|
|
/// </summary>
|
|
public class HTTPMJPEGStreamCamera : BaseHTTPStreamCamera
|
|
{
|
|
// 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;
|
|
|
|
|
|
/// <summary>
|
|
/// Creates a new instance for an IP camera exposing an MJPEG stream over HTTP.
|
|
/// </summary>
|
|
/// <inheritdoc />
|
|
public HTTPMJPEGStreamCamera(Uri streamUri) : base(streamUri)
|
|
{
|
|
}
|
|
|
|
|
|
/// <inheritdoc />
|
|
protected override async Task ReadFrames(ICameraObserver observer, 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;
|
|
}
|
|
|
|
|
|
var frameTimeout = TimeSpan.FromMinutes(1);
|
|
var frameTimeoutCancellationTokenSource = new CancellationTokenSource(frameTimeout);
|
|
var combinedCancellation = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, frameTimeoutCancellationTokenSource.Token);
|
|
|
|
while (!combinedCancellation.IsCancellationRequested)
|
|
{
|
|
var bytesRead = await stream.ReadAsync(buffer, bufferPosition, ChunkSize, combinedCancellation.Token);
|
|
if (bytesRead == 0)
|
|
throw new EndOfStreamException();
|
|
|
|
bufferPosition += bytesRead;
|
|
|
|
if (!startOfImage.HasValue)
|
|
{
|
|
var index = buffer.IndexOf(JpegSOI, bufferPosition);
|
|
if (index == -1)
|
|
{
|
|
// No start of image yet, we need to buffer more
|
|
ExpandBuffer();
|
|
continue;
|
|
}
|
|
|
|
startOfImage = index;
|
|
}
|
|
|
|
var endOfImage = buffer.IndexOf(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;
|
|
|
|
using (var image = new Bitmap(new MemoryStream(buffer, startOfImage.Value, endOfImage - startOfImage.Value)))
|
|
{
|
|
await observer.OnFrame(image);
|
|
}
|
|
|
|
ResetBuffer(endOfImage);
|
|
frameTimeoutCancellationTokenSource.CancelAfter(frameTimeout);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
internal static class BufferExtensions
|
|
{
|
|
public static int IndexOf(this byte[] buffer, byte[] pattern, int limit = int.MaxValue, int startAt = 0)
|
|
{
|
|
var patternIndex = 0;
|
|
int bufferIndex;
|
|
|
|
for (bufferIndex = startAt; bufferIndex < buffer.Length && patternIndex < pattern.Length && bufferIndex < limit; bufferIndex++)
|
|
{
|
|
if (buffer[bufferIndex] == pattern[patternIndex])
|
|
{
|
|
patternIndex++;
|
|
}
|
|
else
|
|
{
|
|
patternIndex = 0;
|
|
}
|
|
}
|
|
|
|
if (patternIndex == pattern.Length)
|
|
return bufferIndex - pattern.Length;
|
|
|
|
return -1;
|
|
}
|
|
}
|
|
}
|