diff --git a/src/MMALSharp.Processing/Handlers/FileStreamCaptureHandler.cs b/src/MMALSharp.Processing/Handlers/FileStreamCaptureHandler.cs
index 3f4f8c74..6bdbd323 100644
--- a/src/MMALSharp.Processing/Handlers/FileStreamCaptureHandler.cs
+++ b/src/MMALSharp.Processing/Handlers/FileStreamCaptureHandler.cs
@@ -9,16 +9,47 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Runtime.CompilerServices;
namespace MMALSharp.Handlers
{
///
/// Processes image data to a .
///
- public class FileStreamCaptureHandler : StreamCaptureHandler, IFileStreamCaptureHandler
+ public class FileStreamCaptureHandler : MemoryStreamCaptureHandler, IFileStreamCaptureHandler
{
private readonly bool _customFilename;
private int _increment;
+ private bool _skippingFirstPartialFrame = true;
+ private bool _continuousCapture;
+
+ ///
+ /// When true, the next full frame will be written. If is not also
+ /// true, this property will be reset to false after writing the image so that only one image is written.
+ ///
+ public bool CaptureNextFrame { get; set; }
+
+ ///
+ /// When true, every frame is written to storage.
+ ///
+ public bool ContinuousCapture
+ {
+ get => _continuousCapture;
+
+ set
+ {
+ _continuousCapture = value;
+ if(_continuousCapture)
+ {
+ CaptureNextFrame = true;
+ }
+ }
+ }
+
+ ///
+ /// Defines the image files' DateTime format string that is applied when the object is constructed with directory and extension arguments.
+ ///
+ public string FilenameDateTimeFormat { get; set; } = "dd-MMM-yy HH-mm-ss";
///
/// A list of files that have been processed by this capture handler.
@@ -36,10 +67,15 @@ public class FileStreamCaptureHandler : StreamCaptureHandler, IFileS
public string Extension { get; set; }
///
- /// The name of the current file associated with the FileStream.
+ /// The filename to write next (if applicable).
///
public string CurrentFilename { get; set; }
+ ///
+ /// The full pathname of the most recently written image file (if any).
+ ///
+ public string LastWrittenPathname { get; set; }
+
///
/// Creates a new instance of the class without provisions for writing to a file. Supports
/// subclasses in which file output is optional.
@@ -51,82 +87,73 @@ public FileStreamCaptureHandler()
///
/// Creates a new instance of the class with the specified directory and filename extension. Filenames will be in the
- /// format "dd-MMM-yy HH-mm-ss" taken from this moment in time.
+ /// format defined by the property.
///
/// The directory to save captured data.
/// The filename extension for saving files.
- public FileStreamCaptureHandler(string directory, string extension)
+ /// When true, every frame is written to a file.
+ public FileStreamCaptureHandler(string directory, string extension, bool continuousCapture = true)
{
this.Directory = directory.TrimEnd('/');
this.Extension = extension.TrimStart('.');
+ this.ContinuousCapture = continuousCapture;
MMALLog.Logger.LogDebug($"{nameof(FileStreamCaptureHandler)} created for directory {this.Directory} and extension {this.Extension}");
System.IO.Directory.CreateDirectory(this.Directory);
-
- var now = DateTime.Now.ToString("dd-MMM-yy HH-mm-ss");
-
- int i = 1;
-
- var fileName = $"{this.Directory}/{now}.{this.Extension}";
-
- while (File.Exists(fileName))
- {
- fileName = $"{this.Directory}/{now} {i}.{this.Extension}";
- i++;
- }
- var fileInfo = new FileInfo(fileName);
-
- this.CurrentFilename = Path.GetFileNameWithoutExtension(fileInfo.Name);
- this.CurrentStream = File.Create(fileName);
+ this.LastWrittenPathname = string.Empty;
+ this.CurrentStream = new MemoryStream();
}
///
- /// Creates a new instance of the class with the specified file path.
+ /// Creates a new instance of the class with the specified file pathname. An auto-incrementing number is added to each
+ /// new filename.
///
/// The absolute full path to save captured data to.
- public FileStreamCaptureHandler(string fullPath)
+ /// When true, every frame is written to a file.
+ public FileStreamCaptureHandler(string fullPath, bool continuousCapture = true)
{
- var fileInfo = new FileInfo(fullPath);
-
- this.Directory = fileInfo.DirectoryName;
- this.CurrentFilename = Path.GetFileNameWithoutExtension(fileInfo.Name);
var ext = fullPath.Split('.').LastOrDefault();
-
if (string.IsNullOrEmpty(ext))
{
throw new ArgumentNullException(nameof(ext), "Could not get file extension from path string.");
}
-
- this.Extension = ext;
- MMALLog.Logger.LogDebug($"{nameof(FileStreamCaptureHandler)} created for directory {this.Directory} and extension {this.Extension}");
+ this.ContinuousCapture = continuousCapture;
+ this.Extension = ext;
+ var fileInfo = new FileInfo(fullPath);
+ this.Directory = fileInfo.DirectoryName;
+ this.CurrentFilename = Path.GetFileNameWithoutExtension(fileInfo.Name);
_customFilename = true;
+ MMALLog.Logger.LogDebug($"{nameof(FileStreamCaptureHandler)} created for pathname {fullPath}");
+
System.IO.Directory.CreateDirectory(this.Directory);
- this.CurrentStream = File.Create(fullPath);
+ this.LastWrittenPathname = string.Empty;
+ this.CurrentStream = new MemoryStream();
}
///
- /// Gets the filename that a FileStream points to.
+ /// Gets the filename of the most recently stored image file.
///
/// The filename.
- public string GetFilename() =>
- (this.CurrentStream != null) ? Path.GetFileNameWithoutExtension(this.CurrentStream.Name) : string.Empty;
+ public string GetFilename() =>
+ (!string.IsNullOrEmpty(this.LastWrittenPathname)) ? Path.GetFileNameWithoutExtension(this.LastWrittenPathname) : string.Empty;
///
- /// Gets the filepath that a FileStream points to.
+ /// Gets the pathname of the most recently stored image file.
///
/// The filepath.
- public string GetFilepath() =>
- this.CurrentStream?.Name ?? string.Empty;
+ public string GetFilepath() =>
+ this.LastWrittenPathname;
///
- /// Creates a new File (FileStream), assigns it to the Stream instance of this class and disposes of any existing stream.
+ /// Outputs the current frame to a file. If a full frame hasn't been captured, a flag is set to capture the frame
+ /// once the end of stream is indicated.
///
public virtual void NewFile()
{
@@ -135,20 +162,24 @@ public virtual void NewFile()
return;
}
- this.CurrentStream?.Dispose();
+ // Wait for EOS
+ if(!CaptureNextFrame)
+ {
+ CaptureNextFrame = true;
+ return;
+ }
- string newFilename = string.Empty;
-
- if (_customFilename)
+ string newFilename;
+ if (!string.IsNullOrEmpty(CurrentFilename))
{
// If we're taking photos from video port, we don't want to be hammering File.Exists as this is added I/O overhead. Camera can take multiple photos per second
// so we can't do this when filename uses the current DateTime.
_increment++;
- newFilename = $"{this.Directory}/{this.CurrentFilename} {_increment}.{this.Extension}";
+ newFilename = $"{this.Directory}/{_customFilename} {_increment}.{this.Extension}";
}
else
{
- string tempFilename = DateTime.Now.ToString("dd-MMM-yy HH-mm-ss");
+ string tempFilename = DateTime.Now.ToString(FilenameDateTimeFormat);
int i = 1;
newFilename = $"{this.Directory}/{tempFilename}.{this.Extension}";
@@ -160,7 +191,34 @@ public virtual void NewFile()
}
}
- this.CurrentStream = File.Create(newFilename);
+ using (FileStream fs = new FileStream(newFilename, FileMode.Create, FileAccess.Write))
+ {
+ this.CurrentStream.WriteTo(fs);
+ }
+
+ this.LastWrittenPathname = newFilename;
+ }
+
+ ///
+ /// If capture is active, output a new file.
+ ///
+ public virtual void NewFrame()
+ {
+ if (_skippingFirstPartialFrame)
+ {
+ _skippingFirstPartialFrame = false;
+ return;
+ }
+
+ if(_continuousCapture || this.CaptureNextFrame)
+ this.NewFile();
+
+ this.CaptureNextFrame = _continuousCapture;
+
+ if (this.CurrentStream != null)
+ {
+ this.CurrentStream.SetLength(0);
+ }
}
///
diff --git a/src/MMALSharp.Processing/Handlers/IFileStreamCaptureHandler.cs b/src/MMALSharp.Processing/Handlers/IFileStreamCaptureHandler.cs
index c708f12c..bfc203a1 100644
--- a/src/MMALSharp.Processing/Handlers/IFileStreamCaptureHandler.cs
+++ b/src/MMALSharp.Processing/Handlers/IFileStreamCaptureHandler.cs
@@ -15,6 +15,11 @@ public interface IFileStreamCaptureHandler : IOutputCaptureHandler
///
void NewFile();
+ ///
+ /// The callback handler has received an end-of-stream marker.
+ ///
+ void NewFrame();
+
///
/// Gets the filepath that a FileStream points to.
///
diff --git a/src/MMALSharp.Processing/Handlers/ImageStreamCaptureHandler.cs b/src/MMALSharp.Processing/Handlers/ImageStreamCaptureHandler.cs
index 9d8599ad..88fbf9a3 100644
--- a/src/MMALSharp.Processing/Handlers/ImageStreamCaptureHandler.cs
+++ b/src/MMALSharp.Processing/Handlers/ImageStreamCaptureHandler.cs
@@ -13,18 +13,22 @@ namespace MMALSharp.Handlers
public class ImageStreamCaptureHandler : FileStreamCaptureHandler
{
///
- /// Creates a new instance of the class with the specified directory and filename extension.
+ /// Creates a new instance of the class with the specified directory and filename extension. Filenames will be in the
+ /// format defined by the property.
///
/// The directory to save captured images.
/// The filename extension for saving files.
- public ImageStreamCaptureHandler(string directory, string extension)
- : base(directory, extension) { }
+ /// When true, every frame is written to a file.
+ public ImageStreamCaptureHandler(string directory, string extension, bool continuousCapture = true)
+ : base(directory, extension, continuousCapture) { }
///
- /// Creates a new instance of the class with the specified file path.
+ /// Creates a new instance of the class with the specified file pathname. An auto-incrementing number is added to each
+ /// new filename.
///
/// The absolute full path to save captured data to.
- public ImageStreamCaptureHandler(string fullPath)
- : base(fullPath) { }
+ /// When true, every frame is written to a file.
+ public ImageStreamCaptureHandler(string fullPath, bool continuousCapture = true)
+ : base(fullPath, continuousCapture) { }
}
}
diff --git a/src/MMALSharp/Callbacks/FastImageOutputCallbackHandler.cs b/src/MMALSharp/Callbacks/FastImageOutputCallbackHandler.cs
index 487b6ea4..735edc93 100644
--- a/src/MMALSharp/Callbacks/FastImageOutputCallbackHandler.cs
+++ b/src/MMALSharp/Callbacks/FastImageOutputCallbackHandler.cs
@@ -32,9 +32,9 @@ public override void Callback(IBuffer buffer)
var eos = buffer.AssertProperty(MMALBufferProperties.MMAL_BUFFER_HEADER_FLAG_FRAME_END) ||
buffer.AssertProperty(MMALBufferProperties.MMAL_BUFFER_HEADER_FLAG_EOS);
- if (eos && this.CaptureHandler is IFileStreamCaptureHandler)
+ if (eos)
{
- ((IFileStreamCaptureHandler)this.CaptureHandler).NewFile();
+ (this.CaptureHandler as IFileStreamCaptureHandler)?.NewFrame();
}
}
}
diff --git a/src/MMALSharp/Config/AnnotateImage.cs b/src/MMALSharp/Config/AnnotateImage.cs
index 18c09740..f943f55e 100644
--- a/src/MMALSharp/Config/AnnotateImage.cs
+++ b/src/MMALSharp/Config/AnnotateImage.cs
@@ -28,6 +28,42 @@ public enum JustifyText
Right
}
+ ///
+ /// Used to ensure the date/time annotations are updated for longer-running operations such as video recording or streaming.
+ ///
+ public enum DateTimeTextRefreshRate
+ {
+ ///
+ /// Do not automatically refresh the and annotations.
+ /// These annotations can be explicitly refreshed by calling .
+ ///
+ Disabled = 0,
+
+ ///
+ /// Typically used when the time is not displayed.
+ /// Update interval is once per minute.
+ ///
+ Daily = 60000,
+
+ ///
+ /// Typically used with the default "HH:mm" .
+ /// Update interval is once per second.
+ ///
+ Minutes = 1000,
+
+ ///
+ /// Useful if the is altered to display seconds, such as "HH:mm:ss".
+ /// Update interval is 250ms.
+ ///
+ Seconds = 250,
+
+ ///
+ /// Useful if the is altered to display fractional seconds, such as "HH:mm:ss.ffff".
+ /// Update interval is 41ms, which approximately equates to 24FPS.
+ ///
+ SubSecond = 41
+ }
+
///
/// The type is for use with the image annotation functionality.
/// This will produce a textual overlay on image stills depending on the options enabled.
@@ -99,6 +135,22 @@ public class AnnotateImage
/// Show the current time.
///
public bool ShowTimeText { get; set; }
+
+ ///
+ /// The DateTime format string applied when is true. The default is "dd/MM/yyyy".
+ ///
+ public string DateFormat { get; set; } = "dd/MM/yyyy";
+
+ ///
+ /// The DateTime format string applied when is true. The default is "HH:mm".
+ ///
+ public string TimeFormat { get; set; } = "HH:mm";
+
+ ///
+ /// The approximate frequency at which date and time annotations are refreshed. The default is
+ /// which matches the resolution of the default .
+ ///
+ public DateTimeTextRefreshRate RefreshRate { get; set; } = DateTimeTextRefreshRate.Minutes;
///
/// Justify annotation text.
diff --git a/src/MMALSharp/MMALCamera.cs b/src/MMALSharp/MMALCamera.cs
index 0f2bd4be..7b3bd814 100644
--- a/src/MMALSharp/MMALCamera.cs
+++ b/src/MMALSharp/MMALCamera.cs
@@ -363,16 +363,35 @@ public async Task TakePictureTimelapse(IFileStreamCaptureHandler handler, MMALEn
this.Camera.SetShutterSpeed(MMALCameraConfig.ShutterSpeed);
+ // Prepare arguments for the annotation-refresh task
+ var ctsRefreshAnnotation = new CancellationTokenSource();
+ var refreshInterval = (int)(MMALCameraConfig.Annotate?.RefreshRate ?? 0);
+ if(!(MMALCameraConfig.Annotate?.ShowDateText ?? false) && !(MMALCameraConfig.Annotate?.ShowTimeText ?? false))
+ {
+ refreshInterval = 0;
+ }
+
// We now begin capturing on the camera, processing will commence based on the pipeline configured.
this.StartCapture(cameraPort);
if (cancellationToken == CancellationToken.None)
{
- await Task.WhenAll(tasks).ConfigureAwait(false);
+ await Task.WhenAny(
+ Task.WhenAll(tasks),
+ RefreshAnnotations(refreshInterval, ctsRefreshAnnotation.Token)
+ ).ConfigureAwait(false);
+
+ ctsRefreshAnnotation.Cancel();
}
else
{
- await Task.WhenAny(Task.WhenAll(tasks), cancellationToken.AsTask()).ConfigureAwait(false);
+ await Task.WhenAny(
+ Task.WhenAll(tasks),
+ RefreshAnnotations(refreshInterval, ctsRefreshAnnotation.Token),
+ cancellationToken.AsTask()
+ ).ConfigureAwait(false);
+
+ ctsRefreshAnnotation.Cancel();
foreach (var component in handlerComponents)
{
@@ -504,6 +523,34 @@ public void Cleanup()
BcmHost.bcm_host_deinit();
}
+ ///
+ /// Periodically invokes to update date/time annotations.
+ ///
+ /// Update frequency in milliseconds, or 0 to disable.
+ /// A CancellationToken to observe while waiting for a task to complete.
+ /// The awaitable Task.
+ private async Task RefreshAnnotations(int msInterval, CancellationToken cancellationToken)
+ {
+ try
+ {
+ if(msInterval == 0)
+ {
+ await Task.Delay(Timeout.Infinite, cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ while(!cancellationToken.IsCancellationRequested)
+ {
+ await Task.Delay(msInterval, cancellationToken).ConfigureAwait(false);
+ this.Camera.SetAnnotateSettings();
+ }
+ }
+ }
+ catch(OperationCanceledException)
+ { // disregard token cancellation
+ }
+ }
+
///
/// Acts as an isolated processor specifically used when capturing raw frames from the camera component.
///
diff --git a/src/MMALSharp/MMALCameraExtensions.cs b/src/MMALSharp/MMALCameraExtensions.cs
index bb9ff429..1ca418b1 100644
--- a/src/MMALSharp/MMALCameraExtensions.cs
+++ b/src/MMALSharp/MMALCameraExtensions.cs
@@ -246,12 +246,12 @@ internal static void SetAnnotateSettings(this MMALCameraComponent camera)
if (MMALCameraConfig.Annotate.ShowTimeText)
{
- sb.Append(DateTime.Now.ToString("HH:mm") + " ");
+ sb.Append(DateTime.Now.ToString(MMALCameraConfig.Annotate.TimeFormat) + " ");
}
if (MMALCameraConfig.Annotate.ShowDateText)
{
- sb.Append(DateTime.Now.ToString("dd/MM/yyyy") + " ");
+ sb.Append(DateTime.Now.ToString(MMALCameraConfig.Annotate.DateFormat) + " ");
}
if (MMALCameraConfig.Annotate.ShowShutterSettings)