Skip to content

Instantly share code, notes, and snippets.

@tor4kichi
Last active April 3, 2024 01:04
Show Gist options
  • Select an option

  • Save tor4kichi/cd76923d4b58bd4105d31aca0bf2d4d6 to your computer and use it in GitHub Desktop.

Select an option

Save tor4kichi/cd76923d4b58bd4105d31aca0bf2d4d6 to your computer and use it in GitHub Desktop.
<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>
<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>
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