Skip to content
This repository has been archived by the owner on Feb 22, 2024. It is now read-only.

New FrameBufferCaptureHandler and revised CircularBufferCaptureHandler #169

Merged
merged 5 commits into from
Aug 30, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 14 additions & 119 deletions src/MMALSharp.Processing/Handlers/CircularBufferCaptureHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
/// <summary>
/// Represents a capture handler working as a circular buffer.
/// </summary>
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;

/// <summary>
/// The circular buffer object responsible for storing image data.
Expand Down Expand Up @@ -81,142 +72,52 @@ 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<byte>(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<byte>(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<byte>(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;
}

/// <inheritdoc/>
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();
}

/// <inheritdoc/>
public void EnableMotionDetection()
{
_shouldDetectMotion = true;

MMALLog.Logger.LogInformation("Enabling motion detection.");
}

/// <inheritdoc/>
public void DisableMotionDetection()
{
_shouldDetectMotion = false;

(_analyser as FrameDiffAnalyser)?.ResetAnalyser();

MMALLog.Logger.LogInformation("Disabling motion detection.");
}

/// <summary>
/// Call to start recording to FileStream.
/// </summary>
/// <param name="initRecording">Optional Action to execute when recording starts, for example, to request an h.264 I-frame.</param>
/// <param name="cancellationToken">When the token is canceled, <see cref="StopRecording"/> is called. If a token is not provided, the caller must stop the recording.</param>
/// <param name="recordNumFrames">Optional number of full frames to record. If value is 0, <paramref name="cancellationToken"/> parameter will be used to manage timeout.</param>
/// <param name="splitFrames">Optional flag to state full frames should be split to new files.</param>
/// <returns>Task representing the recording process if a CancellationToken was provided, otherwise a completed Task.</returns>
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)
{
throw new InvalidOperationException($"Recording unavailable, {nameof(CircularBufferCaptureHandler)} was not created with output-file arguments");
}

_recordToFileStream = true;
_recordNumFrames = recordNumFrames;
_splitFrames = splitFrames;


if (initRecording != null)
{
initRecording.Invoke();
Expand Down Expand Up @@ -251,12 +152,6 @@ public void StopRecording()

_recordToFileStream = false;
_receivedIFrame = false;
_beginRecordFrame = false;
_recordNumFrames = 0;
_numFramesRecorded = 0;
_splitFrames = false;

(_analyser as FrameDiffAnalyser)?.ResetAnalyser();
}

/// <inheritdoc />
Expand Down
162 changes: 162 additions & 0 deletions src/MMALSharp.Processing/Handlers/FrameBufferCaptureHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// <copyright file="FrameBufferCaptureHandler.cs" company="Techyian">
// Copyright (c) Ian Auty and contributors. All rights reserved.
// Licensed under the MIT License. Please see LICENSE.txt for License info.
// </copyright>

using System;
using System.IO;
using MMALSharp.Common;
using MMALSharp.Processors.Motion;

namespace MMALSharp.Handlers
{
/// <summary>
/// A capture handler focused on high-speed frame buffering, either for on-demand snapshots
/// or for motion detection.
/// </summary>
public class FrameBufferCaptureHandler : MemoryStreamCaptureHandler, IMotionCaptureHandler, IVideoCaptureHandler
{
private MotionConfig _motionConfig;
private bool _detectingMotion;
private FrameDiffAnalyser _motionAnalyser;

private bool _waitForFullFrame = true;
private bool _writeFrameRequested = false;

/// <summary>
/// Creates a new <see cref="FrameBufferCaptureHandler"/> optionally configured to write on-demand snapshots.
/// </summary>
/// <param name="directory">Target path for image files</param>
/// <param name="extension">Extension for image files</param>
/// <param name="fileDateTimeFormat">Filename DateTime formatting string</param>
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);
}

/// <summary>
/// Creates a new <see cref="FrameBufferCaptureHandler"/> configured for motion detection using a raw video stream.
/// </summary>
public FrameBufferCaptureHandler()
: base()
{ }

/// <summary>
/// Target directory when <see cref="WriteFrame"/> is invoked without a directory argument.
/// </summary>
public string FileDirectory { get; set; } = string.Empty;

/// <summary>
/// File extension when <see cref="WriteFrame"/> is invoked without an extension argument.
/// </summary>
public string FileExtension { get; set; } = string.Empty;

/// <summary>
/// Filename format when <see cref="WriteFrame"/> is invoked without a format argument.
/// </summary>
public string FileDateTimeFormat { get; set; } = string.Empty;

/// <summary>
/// The filename (without extension) most recently created by <see cref="WriteFrame"/>, if any.
/// </summary>
public string MostRecentFilename { get; set; } = string.Empty;

/// <summary>
/// The full pathname to the most recent file created by <see cref="WriteFrame"/>, if any.
/// </summary>
public string MostRecentPathname { get; set; } = string.Empty;

/// <inheritdoc />
public MotionType MotionType { get; set; } = MotionType.FrameDiff;

/// <summary>
/// Outputs an image file to the specified location and filename.
/// </summary>
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;
}

/// <inheritdoc />
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);
}
}

/// <inheritdoc />
public void ConfigureMotionDetection(MotionConfig config, Action onDetect)
{
_motionConfig = config;
_motionAnalyser = new FrameDiffAnalyser(config, onDetect);
this.EnableMotionDetection();
}

/// <inheritdoc />
public void EnableMotionDetection()
{
_detectingMotion = true;
_motionAnalyser?.ResetAnalyser();
}

/// <inheritdoc />
public void DisableMotionDetection()
{
_detectingMotion = false;
}

/// <inheritdoc />
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;
}
}
}