網路攝影機錄影器
本節將示範如何建立一個利用連接至 PC 的周邊設備之一——網路攝影機的 Extension。
首先,於 RC+ 的停駐視窗中,實現網路攝影機影像預覽的功能(初級篇)。
接著,新增影像錄製功能(中級篇)。錄影將隨著 SPEL+ 程式的啟動開始,並於結束時停止。考量程式可能長時間運行,將每 5 秒建立一個新檔案進行錄影,並僅保留最新的 2 個檔案。
這是在系統中新增類似車用行車記錄器的功能。於設備啟動過程等場合,若能監控機器人作業,當程式意外停止時,可透過錄製的影像事後目視確認發生了什麼狀況。
若 PC 已連接網路,也可搭配錄影資料,發送異常通知等應用。
那麼,讓我們開始吧。
■ 初級篇
請依照「開始使用」的步驟,建立 Extension 的新專案。
- 名稱請設定為 WebCamRecorder。
- 初始功能請勾選Main menu and tool bar item以及Docking window。
- 於 ARM64 版 Windows 上,請將組態設為 x64。
請於 Visual Studio 上進行建置與除錯,確認功能可正常運作。
- 於 RC+ 主選單的擴展標籤下,會新增
WebCamRecorder (xx)(xx 為顯示語言名稱)選單項目,選取該項目後若能顯示停駐視窗即表示設定成功。
- 於 RC+ 主選單的擴展標籤下,會新增
範本確認完成後,請先結束 RC+。
於 Visual Studio 的方案總管中,按兩下 WebCamRecorder 專案,並進行以下變更。
- 請將 TargetFramework 變更為 net8.0-windows10.0.19041.0。
- 如此即可使用 Windows Media Foundation 的 API。以下變更亦與此相關。Windows Media Foundation 是作為 DirectShow 後繼,於 Windows Vista 以後的作業系統中標準搭載的 COM 架構 API 集合。目前尚未納入 .NET 標準函式庫,但可透過下述 Microsoft.Windows.CsWin32 等工具,如同標準函式庫般使用。
- 請新增
<EnableWindowsTargeting>true</EnableWindowsTargeting>這一行。(於<PropertyGroup>內) - 請新增
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>這一行。
- 請將 TargetFramework 變更為 net8.0-windows10.0.19041.0。
請選擇「工具」>「NuGet 套件管理員」>「管理方案的 NuGet 套件」,開啟畫面。
- 於「參考」標籤中搜尋 Microsoft.Windows.CsWin32 套件,安裝最新版穩定版(本書驗證版本為 v0.3.264)。
- Microsoft.Windows.CsWin32 是一套可讓 C# 輕鬆呼叫 Windows API 的函式庫。詳細請參閱https://github.com/microsoft/CsWin32 。
- 於「參考」標籤中搜尋 Microsoft.Windows.CsWin32 套件,安裝最新版穩定版(本書驗證版本為 v0.3.264)。
請於 Visual Studio 的專案中新增 NativeMethods.txt 及 NativeMethods.json 檔案。
這些檔案為使用 Microsoft.Windows.CsWin32 呼叫 Windows API 所必須。
NativeMethods.txt
MFStartup MFShutdown MFCreateAttributes MFEnumDeviceSources MFCreateSourceReaderFromMediaSource MFCreateMediaType MFCreateSinkWriterFromURL MFCreateSample MFCreateAlignedMemoryBuffer (中略) CoInitializeEx CoTaskMemFree COINITNativeMethods.json
{ "$schema": "https://aka.ms/CsWin32.schema.json", "public": true }
請於專案中新增以下檔案。
CameraInfo.cs
- 此為於本 Extension 中,記述代表攝影機的 CameraInfo 類別的檔案。
(前略) namespace WebCamRecorder { /// <summary> /// Camera information /// </summary> public class CameraInfo { /// <summary> /// Friendly name (may not be unique) /// </summary> public string FriendlyName { get; } /// <summary> /// Unique symbolic link /// </summary> public string SymbolicLink { get; } /// <summary> /// Constructor /// </summary> /// <param name="friendlyName">Friendly name</param> /// <param name="symbolicLink">Symbolic link</param> public CameraInfo( string friendlyName, string symbolicLink ) { FriendlyName = friendlyName; SymbolicLink = symbolicLink; } } }CameraInfoCollection.cs
- 此為記述 CameraInfoCollection 類別的檔案,用以表示攝影機清單,並取得指定攝影機的媒體來源(於 Windows Media Foundation 中,為資料處理的入口物件)。
(前略) namespace WebCamRecorder { (中略) /// <summary> /// Camera collection object /// </summary> public sealed class CameraInfoCollection : IDisposable { /// <summary> /// Camera information /// </summary> public List<CameraInfo> CameraInfos = []; /// <summary> /// Source activates /// </summary> private unsafe IMFActivate_unmanaged** _sourceActivates; /// <summary> /// Constructor /// </summary> /// <param name="sourceActivates">Source activates</param> public unsafe CameraInfoCollection( IMFActivate_unmanaged** sourceActivates ) { _sourceActivates = sourceActivates; } /// <summary> /// Create media source for the specified camera /// </summary> /// <param name="cameraInfo">Selected camera</param> /// <returns>Media source object</returns> public unsafe IMFMediaSource? GetMediaSource( CameraInfo cameraInfo ) { var index = CameraInfos.FindIndex( (x) => ( x != null && x.FriendlyName == cameraInfo.FriendlyName && x.SymbolicLink == cameraInfo.SymbolicLink ) ); if (index < 0) { return null; } else { if (Marshal.GetObjectForIUnknown((nint)_sourceActivates[index]) is not IMFActivate managedSourceActivate) { return null; } var mediaSource = managedSourceActivate.ActivateObject(typeof(IMFMediaSource).GUID) as IMFMediaSource; Marshal.ReleaseComObject(managedSourceActivate); return mediaSource; } } (後略)IFrameProcessor.cs
- 此為記述 IFrameProcessor 介面的檔案,用於處理從攝影機取得的影像。
(前略) namespace WebCamRecorder { /// <summary> /// Frame processor interface /// </summary> public interface IFrameProcessor { /// <summary> /// Initialize the processor /// </summary> /// <param name="width">Frame width</param> /// <param name="height">Frame height</param> /// <param name="stride">Frame stride</param> /// <param name="bitRate">Bit rate</param> public void Initialize( uint width, uint height, uint stride, uint bitRate ); /// <summary> /// Termniate the processor /// </summary> public void Terminate(); /// <summary> /// Process the frame /// </summary> /// <param name="frame">Frame data</param> /// <param name="duration">Duration</param> public void Process( byte[] frame, long duration ); /// <summary> /// Request stopping /// </summary> public void Stop(); /// <summary> /// The processing is currently stopping or not /// </summary> public bool IsStopped { get; } } }CameraManager.cs
- 此為記述 CameraManager 類別的檔案,負責進行拍攝,並依序將影像資料傳遞給影像處理器(即上述實作 IFrameProcessor 的類別實例)。
(前略) namespace WebCamRecorder { (中略) /// <summary> /// Camera manager /// </summary> public class CameraManager { /// <summary> /// List of frame processors /// </summary> public List<IFrameProcessor> FrameProcessors { get; } = []; (中略) /// <summary> /// List available cameras /// </summary> /// <returns>Collection object</returns> public unsafe CameraInfoCollection? ListCameras() { (中略) /// <summary> /// Read a frame from source /// </summary> /// <param name="sourceReader">Source reader object</param> /// <param name="frame">Buffer</param> /// <param name="duration">Variable to get duration</param> /// <returns>1: got it, 0: not got, -1: error</returns> private static unsafe int ReadFrame( IMFSourceReader sourceReader, byte[] frame, out long duration ) { (中略) /// <summary> /// Start the processings /// </summary> /// <param name="cameraInfo">Selected camera</param> /// <returns>Task</returns> public async Task Start( CameraInfo cameraInfo ) { while (!_done) { const int _waitMSec = 10; await Task.Delay(_waitMSec); } await Task.Run(() => { HRESULT hr; hr = PInvoke.CoInitializeEx(COINIT.COINIT_MULTITHREADED); if (hr.Failed) { return; } hr = PInvoke.MFStartup(PInvoke.MF_VERSION, PInvoke.MFSTARTUP_FULL); if (hr.Succeeded) { _done = false; var sourceReader = CreateSourceReader(cameraInfo); if (sourceReader != null) { GetVideoInfos( sourceReader, out var width, out var height, out var stride, out var bitRate ); var frame = new byte[stride * height]; foreach (var frameProcessor in FrameProcessors) { frameProcessor.Initialize(width, height, stride, bitRate); } _stopping = false; while (true) { var status = ReadFrame(sourceReader, frame, out var duration); if (status < 0) { break; } else if (status > 0) { foreach (var frameProssor in FrameProcessors) { frameProssor.Process(frame, duration); } } if (_stopping && FrameProcessors.All(x => x.IsStopped)) { break; } } foreach (var frameProcessor in FrameProcessors) { frameProcessor.Terminate(); } Marshal.ReleaseComObject(sourceReader); } _ = PInvoke.MFShutdown(); _done = true; _stoppedAction?.Invoke(); } }); } (後略)Previewer.cs
- 此為記述 Previewer 類別的檔案,用於預覽攝影機影像。
- PreviewImage 為 Image 控制項,用於於視窗中顯示影像。初始化時會建立點陣圖資料,設定至 PreviewImage 的 Source,之後將 CameraManager 傳來的影像資料寫入該點陣圖。
(前略) namespace WebCamRecorder { (中略) /// <summary> /// Image previewer for the camera /// </summary> public class Previewer : IFrameProcessor { /// <summary> /// Image control /// </summary> public Image? PreviewImage; /// <summary> /// Bitmap /// </summary> private WriteableBitmap? _bitmap; (中略) /// <inheritdoc /> public void Initialize( uint width, uint height, uint stride, uint bitRate ) { _width = (int)width; _height = (int)height; _stride = (int)stride; Application.Current.Dispatcher.Invoke(() => { _bitmap = new( _width, _height, 96, 96, PixelFormats.Bgr32, null ); if (PreviewImage != null) { PreviewImage.Source = _bitmap; } }); } (中略) /// <inheritdoc /> public void Process( byte[] frame, long duration ) { if (_bitmap == null) { return; } Application.Current.Dispatcher.Invoke(() => { _bitmap.WritePixels( new Int32Rect(0, 0, _width, _height), frame, _stride, 0 ); }); } (後略)- 此為記述 Previewer 類別的檔案,用於預覽攝影機影像。
請編輯 DockingWindow 資料夾下的 DockingWindowContent.xaml 檔案。
考量影像可能大於視窗,請配置 ScrollViewer,並將 DockPanel 移至其中。
請刪除原有的 TextBlock 與 Grid。
請新增包含 Label 與 ComboBox 的 StackPanel。
- 於本 Extension 中,當顯示 ComboBox 下拉選單時,會(如有進行)中止拍攝處理,並取得已連接攝影機的清單。從下拉選單選擇攝影機後,將開始拍攝處理。這些處理稍後會說明,預計將以下屬性與命令新增至檢視模型,並於對應位置進行繫結。
- 表示攝影機清單的 Cameras(
ReactiveCollection<CameraInfo>) - 表示所選攝影機於攝影機清單中索引的 SelectedCameraIndex(
ReactivePropertySlim<int>) - (重新)取得攝影機清單的命令 RefreshCamerasCommand(ReactiveCommand)
- 表示攝影機清單的 Cameras(
- 請留意 Label 的 Content。已繫結至 Captions[CaptionCamera].Value。於 Extension 中,欲依 RC+ 顯示語言進行在地化的字串,請記述於 Captions.xlsx。詳細將於後述說明,於 Captions.xlsx 的 symbol 欄定義名稱(本例為 CaptionCamera),並同樣進行繫結,即可依 RC+ 顯示語言進行在地化。
- 於本 Extension 中,當顯示 ComboBox 下拉選單時,會(如有進行)中止拍攝處理,並取得已連接攝影機的清單。從下拉選單選擇攝影機後,將開始拍攝處理。這些處理稍後會說明,預計將以下屬性與命令新增至檢視模型,並於對應位置進行繫結。
請新增名為 PreviewImage 的 Image。
<UserControl x:Class="WebCamRecorder.DockingWindow.DockingWindowContent" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:i="http://schemas.microsoft.com/xaml/behaviors" xmlns:local="clr-namespace:WebCamRecorder.DockingWindow" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800"> <UserControl.DataContext> <local:DockingWindowContentViewModel /> </UserControl.DataContext> <ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"> <DockPanel Background="White" LastChildFill="True"> <StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="10"> <Label Content="{Binding Captions[CaptionCamera].Value}" /> <ComboBox ItemsSource="{Binding Cameras}" SelectedIndex="{Binding SelectedCameraIndex.Value}" IsReadOnly="True" DisplayMemberPath="FriendlyName" MinWidth="200" Margin="10,0,0,0"> <i:Interaction.Triggers> <i:EventTrigger EventName="DropDownOpened"> <i:InvokeCommandAction Command="{Binding RefreshCamerasCommand}" /> </i:EventTrigger> </i:Interaction.Triggers> </ComboBox> </StackPanel> <Image x:Name="PreviewImage" Width="640" Height="480" Stretch="UniformToFill" HorizontalAlignment="Left" /> </DockPanel> </ScrollViewer> </UserControl>
請編輯 DockingWindow 資料夾下的 DockingWindowContentViewModelAddition.cs 檔案。
- DockingWindow 資料夾下亦有 DockingWindowContentViewModel.cs 檔案,這兩個檔案共同記述 DockingWindowContentViewModel 類別。
DockingWindowContentViewModel.cs 除了關閉與儲存外,還有複製、剪下、貼上等內容編輯用方法,可視需求撰寫處理。
於本 Extension 中,僅於視窗關閉時新增停止拍攝處理的程式。
(前略) /// <inheritdoc /> public Task<bool> CloseAsync() { _cameraManager.Stop(); return Task.FromResult(true); } (後略)
DockingWindowContentViewModelAddition.cs 包含檢視模型的建構函式,以及於視窗建立後僅會呼叫一次的 WindowCreated 方法。將視窗專屬的屬性、命令及相關 API 呼叫集中於此檔案,可使檢視模型整體結構更清晰。
(前略) namespace WebCamRecorder.DockingWindow { (中略) /// <summary> /// Extension : Docking Window (Specific Part) /// </summary> internal partial class DockingWindowContentViewModel { /// <summary> /// Camera list /// </summary> public ReactiveCollection<CameraInfo> Cameras { get; } = []; /// <summary> /// Index of the selected camera /// </summary> public ReactivePropertySlim<int> SelectedCameraIndex { get; } = new(-1); /// <summary> /// Refresh camera list command /// </summary> public ReactiveCommand RefreshCamerasCommand { get; } = new(); (中略) /// <summary> /// Camera manager /// </summary> private readonly CameraManager _cameraManager = new(); /// <summary> /// Previewer /// </summary> private readonly Previewer _previewer = new(); (中略) /// <summary> /// Refresh camera list /// </summary> private void OnRefreshCameras() { SelectedCameraIndex.Value = -1; Cameras.Clear(); var cameraInfoCollection = _cameraManager.ListCameras(); if (cameraInfoCollection != null) { foreach (var cameraInfo in cameraInfoCollection.CameraInfos) { Cameras.Add(cameraInfo); } cameraInfoCollection.Dispose(); } } /// <summary> /// Change camera /// </summary> /// <param name="index">The index of the selected camera</param> /// <returns>Task</returns> private async Task OnSelectedCameraChanged( int index ) { _cameraManager.Stop(); if (index >= 0) { await _cameraManager.Start(Cameras[index]); } } /// <summary> /// Set image control for previewer /// </summary> /// <param name="previewImage">Image control for previewing</param> public void SetPreviewImage( Image previewImage ) { _previewer.PreviewImage = previewImage; } /// <summary> /// Constructor /// </summary> public DockingWindowContentViewModel() { _cameraManager.FrameProcessors.Add(_previewer); RefreshCamerasCommand.Subscribe(OnRefreshCameras).AddTo(_disposables); SelectedCameraIndex.Subscribe(async (index) => { await OnSelectedCameraChanged(index); }) .AddTo(_disposables); } (後略)
- DockingWindow 資料夾下亦有 DockingWindowContentViewModel.cs 檔案,這兩個檔案共同記述 DockingWindowContentViewModel 類別。
請編輯 DockingWindow 資料夾下的 DockingWindowContent.xaml.cs 檔案。
將預覽用 Image 控制項的瀏覽傳遞給檢視模型。
(前略) if (DataContext is DockingWindowContentViewModel viewModel) { viewModel.SetPreviewImage(PreviewImage); } (後略)
請開啟並編輯 Captions.xlsx 檔案。
- 如前所述,欲依 RC+ 顯示語言在地化的字串,請記述於此檔案。
ID 為標題編號。請確保於本檔案內不重複編號。
description 為註解。可自由填寫。
symbol 為於 Extension 原始碼(.xaml、.cs)中瀏覽用的名稱。編輯 Captions.xlsx 並建置專案後,會產生將 symbol 與 ID 關聯的常數定義檔 Captions.cs。請勿直接編輯此檔案。
// <auto-generated> namespace WebCamRecorder { using System.Reflection; internal class Constants { internal class Caption { (中略) public const int ExtensionName = 0; public const int MainMenu = 1; public const int WindowTitle = 400; public const int CaptionCamera = 401; } } }English、Japanese 等各欄,請填入各語言欲顯示的字串。
- 如前所述,欲依 RC+ 顯示語言在地化的字串,請記述於此檔案。
請進行建置與除錯。
- 請將網路攝影機連接至 PC,顯示 WebCamRecorder 視窗,選擇攝影機並顯示影像即為成功。
■ 中級篇
於初級篇中,除產生的方案所用部分外,尚未使用 Extensions API。
中級篇將嘗試使用提供下列功能的 Extensions API。
- 取得已開啟專案的專案資料夾路徑(專案 API)。
- 取得 SPEL+ 的任務清單(程式執行 API)。
善用 .NET 豐富的函式庫與 Windows API,並搭配必要的 Extensions API,可建立與 RC+ 及 SPEL+ 程式緊密整合的專屬應用程式作為 Extension 使用。
那麼,繼續進行吧。
若 RC+ 已啟動請先結束,啟動 Visual Studio 並開啟初級篇建立的方案。
請新增 Recorder.cs。
Recorder 類別與 Previewer 類別一樣,皆實作 IFrameProcessor。Recorder 會將 CameraManager 傳來的影像資料,產生為 H.264 格式的影片檔案。
影片檔案約每 5 秒於 Recorder 實例所設定路徑的資料夾下,以
Video_N.mp4(N 為 000~999,達 999 後回到 000)命名新建,並僅保留最新 2 個檔案以避免佔用過多儲存空間。- 於本 Extension 中,當開始新錄影時,會刪除資料夾內所有影片。
此外,為實現行車記錄器式的行為,指示停止錄影後仍會持續錄影 2 秒。(即最後儲存的影片最長約為 7 秒。)
錄影模式除可與 SPEL+ 程式啟動/結束連動(Auto)外,亦可於任意時機手動開始/停止(Manual)。
由於預覽攝影機影像時無需明確啟動(選擇攝影機即開始),IFrameProcessor 僅提供停止指示的 Stop 方法。因此,Recorder 會將上述錄影模式作為 Mode 屬性,並於設定 Mode 時啟動錄影。此外,於 Mode 變更時觸發 PropertyChanged 事件,亦可於程式中掌握錄影實際停止的時機。
(前略) namespace WebCamRecorder { (中略) /// <summary> /// Recorder /// </summary> public class Recorder : IFrameProcessor, INotifyPropertyChanged { /// <summary> /// Recording mode definitions /// </summary> public enum RecordingMode { Stop, Auto, Manual, } /// <inheritdoc /> public event PropertyChangedEventHandler? PropertyChanged; /// <summary> /// Recording mode /// </summary> public RecordingMode Mode { get { return _mode; } set { if (_sinkWriter == null) { _shouldStop = false; _mode = value; RaisePropertyChanged(); } } } /// <summary> /// Folder for video files /// </summary> public string VideoFolder { get { return _videoFolder; } set { if (_sinkWriter == null) { try { Directory.CreateDirectory(value); _videoFolder = value; } catch (Exception) { // EMPTY } } } } (中略) /// <inheritdoc /> public void Process( byte[] frame, long duration ) { if (_sinkWriter == null) { if (_mode == RecordingMode.Stop) { return; } _sinkWriter = CreateSinkWriter(GetNextSegmentFile()); _recordTime = 0; _segmentSpan = _initialSegmentSpan; } if (_sinkWriter != null && _sample != null) { SetFlippedFrame(frame); _sample.SetSampleTime(_recordTime); _sample.SetSampleDuration(duration); _sinkWriter.WriteSample(_streamIndex, _sample); _recordTime += duration; if (_recordTime > _segmentSpan) { _sinkWriter.Finalize(); Marshal.ReleaseComObject(_sinkWriter); _sinkWriter = null; if (_shouldStop) { Mode = RecordingMode.Stop; } } } } /// <inheritdoc /> public void Stop() { const long _minAdditionalTime = 20_000_000; if (_segmentSpan - _recordTime < _minAdditionalTime) { _segmentSpan = _recordTime + _minAdditionalTime; } _shouldStop = true; } (後略)
請編輯 DockingWindow 資料夾下的 DockingWindowContent.xaml 檔案。
於畫面上新增顯示錄影中狀態的指示器,以及可於手動(Manual 模式)下開始/停止錄影的按鈕。
於檢視模型中,稍後將新增以下屬性與命令。
- 表示錄影中狀態的 IsRecording(
ReactivePropertySlim<bool>) - 表示可開始錄影的 CanStartRecording(
ReactivePropertySlim<bool>)及指示開始錄影的 StartRecordingCommand(ReactiveCommand) - 表示可停止錄影的 CanStopRecording(
ReactivePropertySlim<bool>)及指示停止錄影的 StopRecordingCommand(ReactiveCommand) - 於本 Extension 中,Auto 模式錄影時,禁止手動開始/停止錄影;反之於 Manual 模式錄影時,Auto 模式錄影將不啟用。
- 但無論哪種情況,關閉停駐視窗即會停止錄影。
(前略) </ComboBox> <Border CornerRadius="10" Width="60" Height="20" Margin="20,0,0,0" VerticalAlignment="Center"> <TextBlock Text="REC" HorizontalAlignment="Center" VerticalAlignment="Center"> <TextBlock.Style> <Style TargetType="TextBlock"> <Style.Triggers> <DataTrigger Binding="{Binding IsRecording.Value}" Value="True"> <Setter Property="Foreground" Value="White" /> </DataTrigger> <DataTrigger Binding="{Binding IsRecording.Value}" Value="False"> <Setter Property="Foreground" Value="Black" /> </DataTrigger> </Style.Triggers> </Style> </TextBlock.Style> </TextBlock> <Border.Style> <Style TargetType="Border"> <Style.Triggers> <DataTrigger Binding="{Binding IsRecording.Value}" Value="True"> <Setter Property="Background" Value="Red" /> </DataTrigger> <DataTrigger Binding="{Binding IsRecording.Value}" Value="False"> <Setter Property="Background" Value="LightGray" /> </DataTrigger> </Style.Triggers> </Style> </Border.Style> </Border> <Button Command="{Binding StartRecordingCommand}" IsEnabled="{Binding CanStartRecording.Value}" Content="{Binding Captions[LabelStart].Value}" Width="80" VerticalAlignment="Center" Margin="10,0,0,0" /> <Button Command="{Binding StopRecordingCommand}" IsEnabled="{Binding CanStopRecording.Value}" Content="{Binding Captions[LabelStop].Value}" Width="80" VerticalAlignment="Center" Margin="10,0,0,0" />- 表示錄影中狀態的 IsRecording(
請編輯 DockingWindow 資料夾下的 DockingWindowContentViewModelAddition.cs 檔案。
於檢視端(.xaml)繫結的屬性與命令請於此新增。
Recorder 類別的實例亦請加入 CameraManager。
請留意 WindowCreated 方法。於此處,利用 Extensions API 的專案 API 取得已開啟專案的專案資料夾(路徑名稱)。
- API 物件可透過 Main.GetAPI 方法取得。
- 專案 API 物件的 ProjectFolder 屬性即為已開啟專案的專案資料夾路徑。若未開啟專案則為 null。
- 若未開啟專案,則改用 Windows 登入使用者的「影片」資料夾。
- 錄影檔案將於專案資料夾或登入使用者的「影片」資料夾下建立 WebCamRecorder 子資料夾,並儲存於其中,Recorder 會進行相關設定。
(前略) /// <summary> /// Recording in progress or not /// </summary> public ReactivePropertySlim<bool> IsRecording { get; } = new(false); /// <summary> /// Can start recording or not /// </summary> public ReactivePropertySlim<bool> CanStartRecording { get; } = new(false); /// <summary> /// Start recording command /// </summary> public ReactiveCommand StartRecordingCommand { get; } /// <summary> /// Can stop recording or not /// </summary> public ReactivePropertySlim<bool> CanStopRecording { get; } = new(false); /// <summary> /// Stop recording command /// </summary> public ReactiveCommand StopRecordingCommand { get; } (中略) /// <summary> /// Recorder /// </summary> private readonly Recorder _recorder = new(); (中略) /// <summary> /// Change camera /// </summary> /// <param name="index">The index of the selected camera</param> /// <returns>Task</returns> private async Task OnSelectedCameraChanged( int index ) { EnableOrDisableRecordingCommands(); _cameraManager.Stop(); if (index >= 0) { await _cameraManager.Start(Cameras[index]); } } (中略) /// <summary> /// Update recording command possibilities /// </summary> private void EnableOrDisableRecordingCommands() { CanStartRecording.Value = (SelectedCameraIndex.Value >= 0 && _recorder.Mode == Recorder.RecordingMode.Stop); CanStopRecording.Value = (_recorder.Mode == Recorder.RecordingMode.Manual); } /// <summary> /// Start recording /// </summary> private void OnStartRecording( bool isAuto ) { if (_recorder.Mode == Recorder.RecordingMode.Stop) { try { var files = Directory.EnumerateFiles( _recorder.VideoFolder, $"*{Recorder.VideoFileExtension}" ); foreach (var file in files) { File.Delete(file); } } catch (Exception) { // IGNORE } _recorder.Mode = isAuto ? Recorder.RecordingMode.Auto : Recorder.RecordingMode.Manual; EnableOrDisableRecordingCommands(); } } /// <summary> /// Stop recording /// </summary> private void OnStopRecording() { _recorder.Stop(); CanStopRecording.Value = false; } /// <summary> /// Constructor /// </summary> public DockingWindowContentViewModel() { _cameraManager.FrameProcessors.Add(_previewer); _cameraManager.FrameProcessors.Add(_recorder); (中略) _recorder.PropertyChanged += (_, _) => { IsRecording.Value = (_recorder.Mode != Recorder.RecordingMode.Stop); EnableOrDisableRecordingCommands(); }; (中略) } /// <inheritdoc /> public Task WindowCreated() { string videoFolder; var projectAPI = Main.GetAPI<IRCXProjectAPI>(); if (projectAPI != null && projectAPI.ProjectFolder != null) { videoFolder = projectAPI.ProjectFolder; } else { videoFolder = Environment.GetFolderPath(Environment.SpecialFolder.MyVideos); } _recorder.VideoFolder = Path.Combine(videoFolder, "WebCamRecorder"); (後略)
請進行建置與除錯。
- 請開啟停駐視窗,選擇攝影機,嘗試開始與停止錄影。

- 若指定資料夾※內有影片檔案且可播放即為成功。
※若有開啟專案則於專案資料夾內的「WebCamRecorder」資料夾
未開啟專案則於 Windows 登入使用者的「影片」資料夾
請編輯 DockingWindow 資料夾下的 DockingWindowContentViewModelAddition.cs 檔案。
新增 Auto 模式錄影功能。為此,必須在 Extension 中掌握 SPEL+ 程式的執行開始與結束時機。
- SPEL+ 程式為多任務。在本 Extension 中,SPEL+ 程式的開始與結束,視為一般任務的開始與結束。
- 任務清單可透過程式執行 API 物件的 Tasks 屬性取得。
- Tasks 是
IEnumerable<IRCXTask>型別的集合,IRCXTask 實例具有表示任務狀態的 State 屬性,以及表示任務類型的 Kind 屬性。 - 當 SPEL+ 的任務發生任何變更時,Tasks 屬性的 PropertyChanged 事件會被觸發。
- Tasks 是
請在 WindowCreated 方法中加入以下程式碼,以監控執行中的一般任務是否存在,並更新
ReactivePropertySlim<bool>型別的 _isProgramRunning。(前略) /// <summary> /// Program execution API object /// </summary> private IRCXProgramExecutionAPI? _programExecutionAPI; /// <summary> /// Program running state /// </summary> private readonly ReactivePropertySlim<bool> _isProgramRunning = new(false, ReactivePropertyMode.DistinctUntilChanged); (中略) /// <inheritdoc /> public Task WindowCreated() { (中略) _programExecutionAPI = Main.GetAPI<IRCXProgramExecutionAPI>(); _programExecutionAPI?.ObserveProperty(x => x.Tasks).Subscribe((tasks) => { _isProgramRunning.Value = tasks .Any( x => ( x.Kind == IRCXProgramExecutionAPI.IRCXTask.RCXTaskKind.Normal && x.State == IRCXProgramExecutionAPI.IRCXTask.RCXTaskState.Run ) ); }) .AddTo(_disposables); (後略)此外,請在建構函式中加入程式碼,偵測上述 _isProgramRunning 屬性的變化,並呼叫錄影的開始與結束。
(前略) /// <summary> /// Constructor /// </summary> public DockingWindowContentViewModel() { (中略) _isProgramRunning.Subscribe((isRunning) => { if (SelectedCameraIndex.Value >= 0) { if (isRunning) { OnStartRecording(isAuto: true); } else { OnStopRecording(); } EnableOrDisableRecordingCommands(); } }) .AddTo(_disposables); } (後略)
請進行建置與除錯。
- 請開啟 Extension 視窗,選擇相機,並確認預覽影像已顯示。
- 請開啟 Run 視窗並執行程式。
- 當程式開始時錄影會啟動,結束後約 2 秒錄影會停止,若影片檔案已儲存至指定資料夾即表示成功。