From a51e81475c106483d71a1d466caee83e47537a8d Mon Sep 17 00:00:00 2001 From: Jon McGuire Date: Sat, 15 Aug 2020 08:48:20 -0400 Subject: [PATCH 1/2] https://github.com/techyian/MMALSharp/issues/163#issuecomment-674384476 --- .../Handlers/FileStreamCaptureHandler.cs | 148 ++++++++++++------ .../Handlers/IFileStreamCaptureHandler.cs | 5 + .../Handlers/ImageStreamCaptureHandler.cs | 16 +- .../FastImageOutputCallbackHandler.cs | 4 +- 4 files changed, 120 insertions(+), 53 deletions(-) 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(); } } } From a2c3f6212cb74d8ec0d60574aed643935c22a70d Mon Sep 17 00:00:00 2001 From: Jon McGuire Date: Sat, 15 Aug 2020 11:32:31 -0400 Subject: [PATCH 2/2] annotation refresh --- src/MMALSharp/Config/AnnotateImage.cs | 52 +++++++++++++++++++++++++++ src/MMALSharp/MMALCamera.cs | 51 ++++++++++++++++++++++++-- src/MMALSharp/MMALCameraExtensions.cs | 4 +-- 3 files changed, 103 insertions(+), 4 deletions(-) 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)