Last active
April 3, 2024 01:04
-
-
Save tor4kichi/cd76923d4b58bd4105d31aca0bf2d4d6 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <myControls:ScreenCapturePlayer CaptureItem="{x:Bind CaptureItem, Mode=OneWay}" | |
| CaptureWindowWidth="{x:Bind Width, Mode=TwoWay}" | |
| CaptureWindowHeight="{x:Bind Height, Mode=TwoWay}" | |
| IsCaptureClosed="{x:Bind IsCaptureClosed, Mode=TwoWay}" | |
| Interval="0:0:0.2" | |
| > | |
| </myControls:ScreenCapturePlayer> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <UserControl x:Class="Starryboard.Views.Controls.ScreenCapturePlayer" | |
| xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" | |
| xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" | |
| xmlns:local="using:Starryboard.Views.Controls" | |
| xmlns:d="http://schemas.microsoft.com/expression/blend/2008" | |
| xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" | |
| mc:Ignorable="d" | |
| d:DesignHeight="300" | |
| d:DesignWidth="400" | |
| xmlns:canvas="using:Microsoft.Graphics.Canvas.UI.Xaml"> | |
| <Grid> | |
| <canvas:CanvasSwapChainPanel x:Name="CanvasPanel" | |
| /> | |
| </Grid> | |
| </UserControl> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using CommunityToolkit.Diagnostics; | |
| using Microsoft.Graphics.Canvas; | |
| using System; | |
| using System.Collections.Generic; | |
| using System.Diagnostics; | |
| using System.IO; | |
| using System.Linq; | |
| using System.Numerics; | |
| using System.Runtime.InteropServices.WindowsRuntime; | |
| using System.ServiceModel.Channels; | |
| using System.Threading.Tasks; | |
| using Windows.Foundation; | |
| using Windows.Foundation.Collections; | |
| using Windows.Graphics; | |
| using Windows.Graphics.Capture; | |
| using Windows.Graphics.DirectX; | |
| using Windows.Graphics.Display; | |
| using Windows.Storage.Streams; | |
| using Windows.System; | |
| using Windows.UI; | |
| using Windows.UI.Xaml; | |
| using Windows.UI.Xaml.Controls; | |
| using Windows.UI.Xaml.Controls.Primitives; | |
| using Windows.UI.Xaml.Data; | |
| using Windows.UI.Xaml.Input; | |
| using Windows.UI.Xaml.Media; | |
| using Windows.UI.Xaml.Navigation; | |
| #nullable enable | |
| namespace Starryboard.Views.Controls; | |
| // スクリーンキャプチャ参考 | |
| // https://learn.microsoft.com/ja-jp/windows/uwp/audio-video-camera/screen-capture | |
| // CanvasSwapChainPanel | |
| // https://microsoft.github.io/Win2D/WinUI3/html/T_Microsoft_Graphics_Canvas_UI_Xaml_CanvasSwapChainPanel.htm | |
| // Canvas系コントロールのメモリリーク回避 | |
| // https://microsoft.github.io/Win2D/WinUI3/html/RefCycles.htm | |
| public sealed partial class ScreenCapturePlayer : UserControl | |
| { | |
| // Capture API objects. | |
| private SizeInt32 _lastSize; | |
| private GraphicsCaptureItem? _item; | |
| private Direct3D11CaptureFramePool? _framePool; | |
| private GraphicsCaptureSession? _session; | |
| // Non-API related members. | |
| private CanvasDevice _canvasDevice; | |
| private CanvasBitmap? _currentFrame; | |
| public ScreenCapturePlayer() | |
| { | |
| this.InitializeComponent(); | |
| _canvasDevice = new CanvasDevice(); | |
| Loaded += ScreenCapturePlayer_Loaded; | |
| Unloaded += ScreenCapturePlayer_Unloaded; | |
| } | |
| private void ScreenCapturePlayer_Unloaded(object sender, RoutedEventArgs e) | |
| { | |
| StopCapture(); | |
| CanvasPanel.RemoveFromVisualTree(); | |
| CanvasPanel = null; | |
| } | |
| private void ScreenCapturePlayer_Loaded(object sender, RoutedEventArgs e) | |
| { | |
| CanvasPanel ??= new(); | |
| } | |
| TimeSpan _lastRelativeTime; | |
| private void StartCaptureInternal(GraphicsCaptureItem item) | |
| { | |
| // Stop the previous capture if we had one. | |
| StopCapture(); | |
| _item = item; | |
| _lastSize = _item.Size; | |
| _framePool = Direct3D11CaptureFramePool.Create( | |
| _canvasDevice, // D3D device | |
| DirectXPixelFormat.B8G8R8A8UIntNormalized, // Pixel format | |
| 2, // Number of frames | |
| _item.Size); // Size of the buffers | |
| CanvasPanel.SwapChain = new CanvasSwapChain(_canvasDevice, _item.Size.Width, _item.Size.Height, DisplayInformation.GetForCurrentView().LogicalDpi); | |
| CanvasPanel.Width = _item.Size.Width; | |
| CanvasPanel.Height = _item.Size.Height; | |
| CaptureWindowWidth = _item.Size.Width; | |
| CaptureWindowHeight = _item.Size.Height; | |
| _framePool.FrameArrived += (s, a) => | |
| { | |
| // The FrameArrived event is raised for every frame on the thread | |
| // that created the Direct3D11CaptureFramePool. This means we | |
| // don't have to do a null-check here, as we know we're the only | |
| // one dequeueing frames in our application. | |
| // NOTE: Disposing the frame retires it and returns | |
| // the buffer to the pool. | |
| using (var frame = _framePool.TryGetNextFrame()) | |
| { | |
| // TryGetNextFrame を毎フレーム呼び出さないとバッファが詰まる模様 | |
| if (!IsEnabled || Opacity == 0 || Visibility == Visibility.Collapsed) { return; } | |
| if (frame.SystemRelativeTime - _lastRelativeTime > Interval) | |
| { | |
| _lastRelativeTime = frame.SystemRelativeTime; | |
| ProcessFrame(frame); | |
| } | |
| } | |
| }; | |
| _item.Closed += (s, a) => | |
| { | |
| StopCapture(); | |
| IsCaptureClosed = true; | |
| }; | |
| IsCaptureClosed = false; | |
| _session = _framePool.CreateCaptureSession(_item); | |
| _session.IsBorderRequired = false; | |
| _session.IsCursorCaptureEnabled = false; | |
| _session.StartCapture(); | |
| } | |
| private void ProcessFrame(Direct3D11CaptureFrame frame) | |
| { | |
| Guard.IsNotNull(_canvasDevice); | |
| if (CanvasPanel?.SwapChain == null) { return; } | |
| // Resize and device-lost leverage the same function on the | |
| // Direct3D11CaptureFramePool. Refactoring it this way avoids | |
| // throwing in the catch block below (device creation could always | |
| // fail) along with ensuring that resize completes successfully and | |
| // isn’t vulnerable to device-lost. | |
| bool needsReset = false; | |
| bool recreateDevice = false; | |
| if ((frame.ContentSize.Width != _lastSize.Width) || | |
| (frame.ContentSize.Height != _lastSize.Height)) | |
| { | |
| needsReset = true; | |
| _lastSize = frame.ContentSize; | |
| } | |
| try | |
| { | |
| // Convert our D3D11 surface into a Win2D object. | |
| CanvasBitmap canvasBitmap = CanvasBitmap.CreateFromDirect3D11Surface( | |
| _canvasDevice, | |
| frame.Surface); | |
| _currentFrame?.Dispose(); | |
| _currentFrame = canvasBitmap; | |
| using (var ds = CanvasPanel.SwapChain.CreateDrawingSession(Colors.Transparent)) | |
| { | |
| ds.DrawImage(canvasBitmap); | |
| } | |
| CanvasPanel.SwapChain.Present(); | |
| } | |
| // This is the device-lost convention for Win2D. | |
| catch (Exception e) when (_canvasDevice.IsDeviceLost(e.HResult)) | |
| { | |
| // We lost our graphics device. Recreate it and reset | |
| // our Direct3D11CaptureFramePool. | |
| needsReset = true; | |
| recreateDevice = true; | |
| } | |
| if (needsReset) | |
| { | |
| ResetFramePool(frame.ContentSize, recreateDevice); | |
| } | |
| } | |
| public void StopCapture() | |
| { | |
| _session?.Dispose(); | |
| _framePool?.Dispose(); | |
| _item = null; | |
| _session = null; | |
| _framePool = null; | |
| _currentFrame?.Dispose(); | |
| if (CanvasPanel?.SwapChain != null) | |
| { | |
| CanvasPanel.SwapChain.Dispose(); | |
| CanvasPanel.SwapChain = null; | |
| } | |
| } | |
| private void ResetFramePool(SizeInt32 size, bool recreateDevice) | |
| { | |
| Guard.IsNotNull(_framePool); | |
| do | |
| { | |
| try | |
| { | |
| if (recreateDevice) | |
| { | |
| _canvasDevice = new CanvasDevice(); | |
| CanvasPanel.SwapChain?.Dispose(); | |
| CanvasPanel.SwapChain = new CanvasSwapChain(_canvasDevice, size.Width, size.Height, DisplayInformation.GetForCurrentView().LogicalDpi); | |
| } | |
| _framePool.Recreate( | |
| _canvasDevice, | |
| DirectXPixelFormat.B8G8R8A8UIntNormalized, | |
| 2, | |
| size); | |
| CanvasPanel.SwapChain.ResizeBuffers(size.Width, size.Height); | |
| CaptureWindowWidth = size.Width; | |
| CaptureWindowHeight = size.Height; | |
| CanvasPanel.Width = size.Width; | |
| CanvasPanel.Height = size.Height; | |
| } | |
| // This is the device-lost convention for Win2D. | |
| catch (Exception e) when (_canvasDevice!.IsDeviceLost(e.HResult)) | |
| { | |
| _canvasDevice = null!; | |
| recreateDevice = true; | |
| } | |
| } while (_canvasDevice == null); | |
| } | |
| #region DP EventHandler | |
| private static void OnCaptureItemPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) | |
| { | |
| var _this = (ScreenCapturePlayer)d; | |
| if (e.NewValue is GraphicsCaptureItem newItem) | |
| { | |
| _this.StartCaptureInternal(newItem); | |
| } | |
| else | |
| { | |
| _this.StopCapture(); | |
| } | |
| } | |
| #endregion | |
| #region DP | |
| public GraphicsCaptureItem? CaptureItem | |
| { | |
| get { return (GraphicsCaptureItem?)GetValue(CaptureItemProperty); } | |
| set { SetValue(CaptureItemProperty, value); } | |
| } | |
| public static readonly DependencyProperty CaptureItemProperty = | |
| DependencyProperty.Register("CaptureItem", typeof(GraphicsCaptureItem), typeof(ScreenCapturePlayer), new PropertyMetadata(null, OnCaptureItemPropertyChanged)); | |
| public double CaptureWindowWidth | |
| { | |
| get { return (double)GetValue(CaptureWindowWidthProperty); } | |
| set { SetValue(CaptureWindowWidthProperty, value); } | |
| } | |
| public static readonly DependencyProperty CaptureWindowWidthProperty = | |
| DependencyProperty.Register("CaptureWindowWidth", typeof(double), typeof(ScreenCapturePlayer), new PropertyMetadata(0d)); | |
| public double CaptureWindowHeight | |
| { | |
| get { return (double)GetValue(CaptureWindowHeightProperty); } | |
| set { SetValue(CaptureWindowHeightProperty, value); } | |
| } | |
| public static readonly DependencyProperty CaptureWindowHeightProperty = | |
| DependencyProperty.Register("CaptureWindowHeight", typeof(double), typeof(ScreenCapturePlayer), new PropertyMetadata(0d)); | |
| public bool IsCaptureClosed | |
| { | |
| get { return (bool)GetValue(IsCaptureClosedProperty); } | |
| set { SetValue(IsCaptureClosedProperty, value); } | |
| } | |
| public static readonly DependencyProperty IsCaptureClosedProperty = | |
| DependencyProperty.Register("IsCaptureClosed", typeof(bool), typeof(ScreenCapturePlayer), new PropertyMetadata(false)); | |
| public TimeSpan Interval | |
| { | |
| get { return (TimeSpan)GetValue(IntervalProperty); } | |
| set { SetValue(IntervalProperty, value); } | |
| } | |
| public static readonly DependencyProperty IntervalProperty = | |
| DependencyProperty.Register("Interval", typeof(TimeSpan), typeof(ScreenCapturePlayer), new PropertyMetadata(TimeSpan.FromSeconds(1/5d))); | |
| #endregion | |
| public async Task SaveImageAsync(IRandomAccessStream stream, CanvasBitmapFileFormat fileFormat) | |
| { | |
| Guard.IsNotNull(_currentFrame); | |
| await _currentFrame.SaveAsync(stream, fileFormat); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment