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 { /// /// Implements the ICamera interface for IP cameras exposing an MJPEG stream over HTTP. /// 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; /// /// Creates a new instance for an IP camera exposing an MJPEG stream over HTTP. /// /// public HTTPMJPEGStreamCamera(Uri streamUri) : base(streamUri) { } /// 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; } } }