diff --git a/src/MMALSharp.Processing/Handlers/CircularBufferCaptureHandler.cs b/src/MMALSharp.Processing/Handlers/CircularBufferCaptureHandler.cs index 2e9ed910..43ffa50a 100644 --- a/src/MMALSharp.Processing/Handlers/CircularBufferCaptureHandler.cs +++ b/src/MMALSharp.Processing/Handlers/CircularBufferCaptureHandler.cs @@ -9,26 +9,17 @@ using Microsoft.Extensions.Logging; using MMALSharp.Common; using MMALSharp.Common.Utility; -using MMALSharp.Processors; -using MMALSharp.Processors.Motion; namespace MMALSharp.Handlers { /// /// Represents a capture handler working as a circular buffer. /// - public sealed class CircularBufferCaptureHandler : VideoStreamCaptureHandler, IMotionCaptureHandler + public sealed class CircularBufferCaptureHandler : VideoStreamCaptureHandler { private bool _recordToFileStream; private int _bufferSize; - private bool _shouldDetectMotion; private bool _receivedIFrame; - private int _recordNumFrames; - private int _numFramesRecorded; - private bool _splitFrames; - private bool _beginRecordFrame; - private IFrameAnalyser _analyser; - private MotionConfig _motionConfig; /// /// The circular buffer object responsible for storing image data. @@ -81,132 +72,44 @@ public override void Process(ImageContext context) this.Buffer.PushBack(context.Data[i]); } } - else if (_recordNumFrames > 0) - { - // We will begin storing data immediately after we receive an EOS, this means we're sure to record frame data from the beginning of the stream. - if (_beginRecordFrame) - { - this.CurrentStream.Write(context.Data, 0, context.Data.Length); - this.Processed += context.Data.Length; - - if (context.Eos) - { - // We've reached the end of the frame. Check if we want to create a new file and increment number of recorded frames. - _numFramesRecorded++; - - if (_numFramesRecorded >= _recordNumFrames) - { - // Effectively stop recording individual frames at this point. - _beginRecordFrame = false; - } - } - } - - if (context.Eos && _numFramesRecorded < _recordNumFrames) - { - _beginRecordFrame = true; - - if (_splitFrames) - { - this.Split(); - } - } - } else { if (context.Encoding == MMALEncoding.H264) { - if (context.IFrame) - { - _receivedIFrame = true; - } + _receivedIFrame = context.IFrame; + } - if (_receivedIFrame && this.Buffer.Size > 0) + if (this.Buffer.Size > 0) + { + // The buffer contains data. + if (this.CurrentStream != null && this.CurrentStream.CanWrite) { - // The buffer contains data. - MMALLog.Logger.LogInformation($"Buffer contains data. Writing {this.Buffer.Size} bytes."); this.CurrentStream.Write(this.Buffer.ToArray(), 0, this.Buffer.Size); - this.Processed += this.Buffer.Size; - this.Buffer = new CircularBuffer(this.Buffer.Capacity); } - if (_receivedIFrame) - { - // We need to have received an IFrame for the recording to be valid. - this.CurrentStream.Write(context.Data, 0, context.Data.Length); - this.Processed += context.Data.Length; - } + this.Processed += this.Buffer.Size; + this.Buffer = new CircularBuffer(this.Buffer.Capacity); } - else - { - if (this.Buffer.Size > 0) - { - // The buffer contains data. - this.CurrentStream.Write(this.Buffer.ToArray(), 0, this.Buffer.Size); - this.Processed += this.Buffer.Size; - this.Buffer = new CircularBuffer(this.Buffer.Capacity); - } + if (this.CurrentStream != null && this.CurrentStream.CanWrite) + { this.CurrentStream.Write(context.Data, 0, context.Data.Length); - this.Processed += context.Data.Length; } - } - if (_shouldDetectMotion && !_recordToFileStream) - { - _analyser?.Apply(context); + this.Processed += context.Data.Length; } // Not calling base method to stop data being written to the stream when not recording. this.ImageContext = context; } - /// - public void ConfigureMotionDetection(MotionConfig config, Action onDetect) - { - _motionConfig = config; - - switch(this.MotionType) - { - case MotionType.FrameDiff: - _analyser = new FrameDiffAnalyser(config, onDetect); - break; - - case MotionType.MotionVector: - // TODO Motion vector analyser - break; - } - - this.EnableMotionDetection(); - } - - /// - public void EnableMotionDetection() - { - _shouldDetectMotion = true; - - MMALLog.Logger.LogInformation("Enabling motion detection."); - } - - /// - public void DisableMotionDetection() - { - _shouldDetectMotion = false; - - (_analyser as FrameDiffAnalyser)?.ResetAnalyser(); - - MMALLog.Logger.LogInformation("Disabling motion detection."); - } - /// /// Call to start recording to FileStream. /// /// Optional Action to execute when recording starts, for example, to request an h.264 I-frame. /// When the token is canceled, is called. If a token is not provided, the caller must stop the recording. - /// Optional number of full frames to record. If value is 0, parameter will be used to manage timeout. - /// Optional flag to state full frames should be split to new files. /// Task representing the recording process if a CancellationToken was provided, otherwise a completed Task. - public async Task StartRecording(Action initRecording = null, CancellationToken cancellationToken = default, int recordNumFrames = 0, bool splitFrames = false) + public async Task StartRecording(Action initRecording = null, CancellationToken cancellationToken = default) { if (this.CurrentStream == null) { @@ -214,9 +117,7 @@ public async Task StartRecording(Action initRecording = null, CancellationToken } _recordToFileStream = true; - _recordNumFrames = recordNumFrames; - _splitFrames = splitFrames; - + if (initRecording != null) { initRecording.Invoke(); @@ -251,12 +152,6 @@ public void StopRecording() _recordToFileStream = false; _receivedIFrame = false; - _beginRecordFrame = false; - _recordNumFrames = 0; - _numFramesRecorded = 0; - _splitFrames = false; - - (_analyser as FrameDiffAnalyser)?.ResetAnalyser(); } /// diff --git a/src/MMALSharp.Processing/Handlers/FrameBufferCaptureHandler.cs b/src/MMALSharp.Processing/Handlers/FrameBufferCaptureHandler.cs new file mode 100644 index 00000000..7a3ab7a5 --- /dev/null +++ b/src/MMALSharp.Processing/Handlers/FrameBufferCaptureHandler.cs @@ -0,0 +1,162 @@ +// +// Copyright (c) Ian Auty and contributors. All rights reserved. +// Licensed under the MIT License. Please see LICENSE.txt for License info. +// + +using System; +using System.IO; +using MMALSharp.Common; +using MMALSharp.Processors.Motion; + +namespace MMALSharp.Handlers +{ + /// + /// A capture handler focused on high-speed frame buffering, either for on-demand snapshots + /// or for motion detection. + /// + public class FrameBufferCaptureHandler : MemoryStreamCaptureHandler, IMotionCaptureHandler, IVideoCaptureHandler + { + private MotionConfig _motionConfig; + private bool _detectingMotion; + private FrameDiffAnalyser _motionAnalyser; + + private bool _waitForFullFrame = true; + private bool _writeFrameRequested = false; + + /// + /// Creates a new optionally configured to write on-demand snapshots. + /// + /// Target path for image files + /// Extension for image files + /// Filename DateTime formatting string + public FrameBufferCaptureHandler(string directory = "", string extension = "", string fileDateTimeFormat = "yyyy-MM-dd HH.mm.ss.ffff") + : base() + { + this.FileDirectory = directory.TrimEnd('/'); + this.FileExtension = extension; + this.FileDateTimeFormat = fileDateTimeFormat; + Directory.CreateDirectory(this.FileDirectory); + } + + /// + /// Creates a new configured for motion detection using a raw video stream. + /// + public FrameBufferCaptureHandler() + : base() + { } + + /// + /// Target directory when is invoked without a directory argument. + /// + public string FileDirectory { get; set; } = string.Empty; + + /// + /// File extension when is invoked without an extension argument. + /// + public string FileExtension { get; set; } = string.Empty; + + /// + /// Filename format when is invoked without a format argument. + /// + public string FileDateTimeFormat { get; set; } = string.Empty; + + /// + /// The filename (without extension) most recently created by , if any. + /// + public string MostRecentFilename { get; set; } = string.Empty; + + /// + /// The full pathname to the most recent file created by , if any. + /// + public string MostRecentPathname { get; set; } = string.Empty; + + /// + public MotionType MotionType { get; set; } = MotionType.FrameDiff; + + /// + /// Outputs an image file to the specified location and filename. + /// + public void WriteFrame() + { + if (string.IsNullOrWhiteSpace(this.FileDirectory) || string.IsNullOrWhiteSpace(this.FileDateTimeFormat)) + throw new Exception($"The {nameof(FileDirectory)} and {nameof(FileDateTimeFormat)} must be set before calling {nameof(WriteFrame)}"); + + _writeFrameRequested = true; + } + + /// + public override void Process(ImageContext context) + { + // guard against partial frame data at startup + if (_waitForFullFrame) + { + _waitForFullFrame = !context.Eos; + if (_waitForFullFrame) + { + return; + } + } + + if (_detectingMotion) + { + _motionAnalyser.Apply(context); + } + + // accumulate frame data in the underlying memory stream + base.Process(context); + + if (context.Eos) + { + // write a full frame if a request is pending + if (_writeFrameRequested) + { + this.WriteStreamToFile(); + _writeFrameRequested = false; + } + + // reset the stream to begin the next frame + this.CurrentStream.SetLength(0); + } + } + + /// + public void ConfigureMotionDetection(MotionConfig config, Action onDetect) + { + _motionConfig = config; + _motionAnalyser = new FrameDiffAnalyser(config, onDetect); + this.EnableMotionDetection(); + } + + /// + public void EnableMotionDetection() + { + _detectingMotion = true; + _motionAnalyser?.ResetAnalyser(); + } + + /// + public void DisableMotionDetection() + { + _detectingMotion = false; + } + + /// + public void Split() + { } // Unused, but required to handle a video stream. + + private void WriteStreamToFile() + { + string directory = this.FileDirectory.TrimEnd('/'); + string filename = DateTime.Now.ToString(this.FileDateTimeFormat); + string pathname = $"{directory}/{filename}.{this.FileExtension}"; + + using (var fs = new FileStream(pathname, FileMode.Create, FileAccess.Write)) + { + CurrentStream.WriteTo(fs); + } + + this.MostRecentFilename = filename; + this.MostRecentPathname = pathname; + } + } +}