Bildschirmaufnahme in Video
In diesem Artikel wird beschrieben, wie Frames codiert werden, die vom Bildschirm mit den Windows.Graphics.Capture-APIs in eine Videodatei aufgenommen werden. Informationen zum Aufzeichnen von Bildschirmfotos finden Sie unter "Bildschirmaufnahme". Eine einfache End-to-End-Beispiel-App, die die in diesem Artikel gezeigten Konzepte und Techniken verwendet, finden Sie unter "SimpleRecorder".
Übersicht über den Videoaufnahmeprozess
Dieser Artikel enthält eine exemplarische Vorgehensweise einer Beispiel-App, die den Inhalt eines Fensters in einer Videodatei aufzeichnet. Auch wenn es den Anschein hat, dass für die Implementierung dieses Szenarios eine Menge Code erforderlich ist, ist die übergeordnete Struktur einer Bildschirmrekorder-App recht einfach. Der Bildschirmaufnahmeprozess verwendet drei primäre UWP-Features:
- Die Windows.GraphicsCapture-APIs erledigen die Arbeit, die Pixel tatsächlich vom Bildschirm zu erfassen. Die GraphicsCaptureItem-Klasse stellt das Fenster oder die Anzeige dar, die erfasst wird. GraphicsCaptureSession wird verwendet, um den Aufnahmevorgang zu starten und zu beenden. Die Direct3D11CaptureFramePool-Klasse verwaltet einen Puffer mit Frames, in den die Bildschirminhalte kopiert werden.
- Die MediaStreamSource-Klasse empfängt die aufgenommenen Frames und generiert einen Videostream.
- Die MediaTranscoder-Klasse empfängt den von MediaStreamSource erzeugten Datenstrom und codiert ihn in einer Videodatei.
Der in diesem Artikel gezeigte Beispielcode kann in einige verschiedene Aufgaben unterteilt werden:
- Initialisierung - Dazu gehört die Konfiguration der oben beschriebenen UWP-Klassen, die Initialisierung der Grafikgeräteschnittstellen, die Auswahl eines Fensters zur Aufnahme und die Einrichtung der Kodierungsparameter wie Auflösung und Bildrate.
- Ereignishandler und Threading – Der primäre Treiber der Standard-Aufnahmeschleife ist die MediaStreamSource, die Frames regelmäßig über das SampleRequested-Ereignis anfordert. In diesem Beispiel werden Ereignisse verwendet, um die Anforderungen für neue Frames zwischen den verschiedenen Komponenten des Beispiels zu koordinieren. Die Synchronisierung ist wichtig, damit Frames gleichzeitig erfasst und codiert werden können.
- Kopieren von Frames – Frames werden aus dem Aufnahmeframepuffer in eine separate Direct3D-Oberfläche kopiert, die an die MediaStreamSource übergeben werden kann, sodass die Ressource beim Codieren nicht überschrieben wird. Direct3D-APIs werden verwendet, um diesen Kopiervorgang schnell auszuführen.
Informationen zu Direct3D-APIs
Wie oben erwähnt, ist das Kopieren jedes aufgenommenen Frames wahrscheinlich der komplexeste Teil der Implementierung, die in diesem Artikel gezeigt wird. Auf niedriger Ebene erfolgt dieser Vorgang mit Direct3D. In diesem Beispiel verwenden wir die SharpDX-Bibliothek, um die Direct3D-Vorgänge aus C# auszuführen. Diese Bibliothek wird nicht mehr offiziell unterstützt, wurde aber ausgewählt, da sie bei Kopiervorgängen auf niedriger Ebene gut für dieses Szenario geeignet ist. Wir haben versucht, die Direct3D-Vorgänge so diskret wie möglich zu halten, damit Sie diese Aufgaben leichter durch Ihren eigenen Code oder andere Bibliotheken ersetzen können.
Einrichten Ihres Projekts
Der Beispielcode in dieser exemplarischen Vorgehensweise wurde mit der C#-Projektvorlage Leere App (Universal Windows) in Visual Studio 2019 erstellt. Um die Windows.Graphics.Capture-APIs in Ihrer App verwenden zu können, müssen Sie die Grafikerfassung in die Datei Package.appxmanifest für Ihr Projekt einschließen. In diesem Beispiel werden generierte Videodateien in der Videobibliothek auf dem Gerät gespeichert. Um auf diesen Ordner zuzugreifen, müssen Sie die Videobibliothek einschließen.
Um das SharpDX Nuget-Paket zu installieren, wählen Sie in Visual Studio Nuget-Pakete verwalten aus. Suchen Sie auf der Registerkarte Durchsuchen nach dem Paket "SharpDX.Direct3D11 und klicken Sie auf Installieren.
Beachten Sie, dass der Code in der folgenden exemplarischen Vorgehensweise explizite Namespace-Referenzen und die Deklaration von Mitgliedsvariablen der MainPage-Klasse weglässt, die mit einem führenden Unterstrich „_“ benannt sind, um die Größe der Codelisten in diesem Artikel zu reduzieren.
Einrichtung für die Codierung
Die in diesem Abschnitt beschriebene Methode SetupEncoding initialisiert einige der Hauptobjekte, die zum Erfassen und Kodieren von Videobildern verwendet werden, und richtet die Kodierungsparameter für aufgenommene Videos ein. Diese Methode kann programmgesteuert oder als Reaktion auf eine Benutzerinteraktion wie ein Schaltflächenklick aufgerufen werden. Die Codeauflistung für SetupEncoding wird unten nach den Beschreibungen der Initialisierungsschritte angezeigt.
Überprüfen Sie die Unterstützung für die Erfassung. Bevor Sie mit dem Aufnahmevorgang beginnen, müssen Sie GraphicsCaptureSession.IsSupported aufrufen, um sicherzustellen, dass die Bildschirmaufnahmefunktion auf dem aktuellen Gerät unterstützt wird.
Initialisieren von Direct3D-Schnittstellen. In diesem Beispiel wird Direct3D verwendet, um die vom Bildschirm erfassten Pixel in eine Textur zu kopieren, die als Videoframe codiert ist. Die Hilfsmethoden zum Initialisieren der Direct3D-Schnittstellen, CreateD3DDevice und CreateSharpDXDevice werden weiter unten in diesem Artikel gezeigt.
Initialisieren Sie ein GraphicsCaptureItem-Objekt. Ein GraphicsCaptureItem stellt ein Element auf dem Bildschirm dar, das entweder ein Fenster oder der gesamte Bildschirm erfasst wird. Lassen Sie zu, dass der Benutzer ein Element zum Erfassen auswählt, indem er eine GraphicsCapturePicker erstellt und PickSingleItemAsync aufruft.
Erstellen Sie eine Kompositionstextur. Erstellen Sie eine Texturressource und eine zugeordnete Renderzielansicht, die zum Kopieren der einzelnen Videoframes verwendet wird. Diese Textur kann erst erstellt werden, bis GraphicsCaptureItem erstellt wurde, und wir kennen die Dimensionen. Sehen Sie sich die Beschreibung des WaitForNewFrame an, um zu sehen, wie diese Kompositionstextur verwendet wird. Die Hilfsmethode zum Erstellen dieser Textur wird weiter unten in diesem Artikel gezeigt.
Erstellen Sie einen MediaEncodingProfile- und VideoStreamDescriptor. Eine Instanz der MediaStreamSource-Klasse erfasste Bilder auf dem Bildschirm und codiert sie in einen Videostream. Anschließend wird der Videostream von der MediaTranscoder-Klasse in eine Videodatei transcodiert. Ein VideoStreamDecriptor stellt Codierungsparameter wie Auflösung und Bildfrequenz für MediaStreamSourcebereit. Die Videodateicodierungsparameter für den MediaTranscoder werden mit einem MediaEncodingProfile angegeben. Beachten Sie, dass die für die Videocodierung verwendete Größe nicht mit der Größe des aufgezeichneten Fensters identisch sein muss, aber um dieses Beispiel einfach zu halten, sind die Codierungseinstellungen hartcodiert, um die tatsächlichen Dimensionen des Erfassungselements zu verwenden.
Erstellen Sie die MediaStreamSource- und MediaTranscoder-Objekte. Wie oben Erwähnt, codiert das MediaStreamSource-Objekt einzelne Frames in einen Videostream. Rufen Sie den Konstruktor für diese Klasse auf und übergeben Sie dabei das im vorherigen Schritt erstellte MediaEncodingProfile. Legen Sie die Pufferzeit auf Null fest, und registrieren Sie Handler für die Ereignisse Starting und SampleRequested, die weiter unten in diesem Artikel gezeigt werden. Erstellen Sie als Nächstes eine neue Instanz der MediaTranscoder-Klasse und aktivieren Sie die Hardwarebeschleunigung.
Erstellen Sie eine Ausgabedatei Der letzte Schritt in dieser Methode besteht darin, eine Datei zu erstellen, in die das Video transcodiert wird. In diesem Beispiel erstellen wir einfach eine eindeutig benannte Datei im Ordner "Videos Library" auf dem Gerät. Beachten Sie, dass Ihre App für den Zugriff auf diesen Ordner die Funktion "Videos Library" im App-Manifest angeben muss. Nachdem die Datei erstellt wurde, öffnen Sie sie zum Lesen und Schreiben, und übergeben Sie den resultierenden Datenstrom an die EncodeAsync-Methode, die als Nächstes angezeigt wird.
private async Task SetupEncoding()
{
if (!GraphicsCaptureSession.IsSupported())
{
// Show message to user that screen capture is unsupported
return;
}
// Create the D3D device and SharpDX device
if (_device == null)
{
_device = Direct3D11Helpers.CreateD3DDevice();
}
if (_sharpDxD3dDevice == null)
{
_sharpDxD3dDevice = Direct3D11Helpers.CreateSharpDXDevice(_device);
}
try
{
// Let the user pick an item to capture
var picker = new GraphicsCapturePicker();
_captureItem = await picker.PickSingleItemAsync();
if (_captureItem == null)
{
return;
}
// Initialize a blank texture and render target view for copying frames, using the same size as the capture item
_composeTexture = Direct3D11Helpers.InitializeComposeTexture(_sharpDxD3dDevice, _captureItem.Size);
_composeRenderTargetView = new SharpDX.Direct3D11.RenderTargetView(_sharpDxD3dDevice, _composeTexture);
// This example encodes video using the item's actual size.
var width = (uint)_captureItem.Size.Width;
var height = (uint)_captureItem.Size.Height;
// Make sure the dimensions are are even. Required by some encoders.
width = (width % 2 == 0) ? width : width + 1;
height = (height % 2 == 0) ? height : height + 1;
var temp = MediaEncodingProfile.CreateMp4(VideoEncodingQuality.HD1080p);
var bitrate = temp.Video.Bitrate;
uint framerate = 30;
_encodingProfile = new MediaEncodingProfile();
_encodingProfile.Container.Subtype = "MPEG4";
_encodingProfile.Video.Subtype = "H264";
_encodingProfile.Video.Width = width;
_encodingProfile.Video.Height = height;
_encodingProfile.Video.Bitrate = bitrate;
_encodingProfile.Video.FrameRate.Numerator = framerate;
_encodingProfile.Video.FrameRate.Denominator = 1;
_encodingProfile.Video.PixelAspectRatio.Numerator = 1;
_encodingProfile.Video.PixelAspectRatio.Denominator = 1;
var videoProperties = VideoEncodingProperties.CreateUncompressed(MediaEncodingSubtypes.Bgra8, width, height);
_videoDescriptor = new VideoStreamDescriptor(videoProperties);
// Create our MediaStreamSource
_mediaStreamSource = new MediaStreamSource(_videoDescriptor);
_mediaStreamSource.BufferTime = TimeSpan.FromSeconds(0);
_mediaStreamSource.Starting += OnMediaStreamSourceStarting;
_mediaStreamSource.SampleRequested += OnMediaStreamSourceSampleRequested;
// Create our transcoder
_transcoder = new MediaTranscoder();
_transcoder.HardwareAccelerationEnabled = true;
// Create a destination file - Access to the VideosLibrary requires the "Videos Library" capability
var folder = KnownFolders.VideosLibrary;
var name = DateTime.Now.ToString("yyyyMMdd-HHmm-ss");
var file = await folder.CreateFileAsync($"{name}.mp4");
using (var stream = await file.OpenAsync(FileAccessMode.ReadWrite))
await EncodeAsync(stream);
}
catch (Exception ex)
{
return;
}
}
Codierung starten
Nachdem die Hauptobjekte initialisiert wurden, wird die EncodeAsync-Methode implementiert, um den Erfassungsvorgang zu starten. Diese Methode überprüft zunächst, ob wir noch keine Aufzeichnung durchführen. Andernfalls ruft sie die Hilfsmethode StartCapture auf, um mit der Erfassung von Frames vom Bildschirm zu beginnen. Diese Methode wird weiter unten in diesem Artikel gezeigt. Als Nächstes wird PrepareMediaStreamSourceTranscodeAsyncaufgerufen, um den MediaTranscoder zum Transcodieren des vom MediaStreamSource-Objekt erzeugten Videodatenstroms in den Ausgabedateidatenstrom mithilfe des Codierungsprofils abzurufen, das wir im vorherigen Abschnitt erstellt haben. Nachdem der Transcoder vorbereitet wurde, rufen Sie TranscodeAsync auf, um die Transcodierung zu starten. Weitere Informationen zur Verwendung von MediaTranscoder finden Sie unter Transcode-Mediendateien.
private async Task EncodeAsync(IRandomAccessStream stream)
{
if (!_isRecording)
{
_isRecording = true;
StartCapture();
var transcode = await _transcoder.PrepareMediaStreamSourceTranscodeAsync(_mediaStreamSource, stream, _encodingProfile);
await transcode.TranscodeAsync();
}
}
Behandeln von MediaStreamSource-Ereignissen
Das MediaStreamSource-Objekt verwendet Frames, die wir vom Bildschirm erfassen, und wandelt sie in einen Videostream um, der mithilfe des MediaTranscoder in einer Datei gespeichert werden kann. Wir übergeben die Frames über Handler für die Ereignisse des Objekts an die MediaStreamSource .
Das SampleRequested-Ereignis wird ausgelöst, wenn die MediaStreamSource für einen neuen Videoframe bereit ist. Nachdem wir sichergestellt haben, dass wir momentan aufzeichnen, wird die Hilfsmethode WaitForNewFrame aufgerufen, um einen neuen Frame abzurufen, der vom Bildschirm erfasst wird. Diese Methode, die weiter unten in diesem Artikel gezeigt wird, gibt ein ID3D11Surface-Objekt zurück, das den erfassten Frame enthält. In diesem Beispiel wird die IDirect3DSurface-Schnittstelle in eine Hilfsklasse umgebrochen, die auch die Systemzeit speichert, zu der der Frame erfasst wurde. Sowohl der Frame als auch die Systemzeit werden an die MediaStreamSample.CreateFromDirect3D11Surface-Factorymethode übergeben, und der resultierende MediaStreamSample wird auf die MediaStreamSourceSampleRequest.Sample-Eigenschaft der MediaStreamSourceSampleRequestedEventArgs festgelegt. So wird der aufgenommene Frame der MediaStreamSource bereitgestellt.
private void OnMediaStreamSourceSampleRequested(MediaStreamSource sender, MediaStreamSourceSampleRequestedEventArgs args)
{
if (_isRecording && !_closed)
{
try
{
using (var frame = WaitForNewFrame())
{
if (frame == null)
{
args.Request.Sample = null;
Stop();
Cleanup();
return;
}
var timeStamp = frame.SystemRelativeTime;
var sample = MediaStreamSample.CreateFromDirect3D11Surface(frame.Surface, timeStamp);
args.Request.Sample = sample;
}
}
catch (Exception e)
{
Debug.WriteLine(e.Message);
Debug.WriteLine(e.StackTrace);
Debug.WriteLine(e);
args.Request.Sample = null;
Stop();
Cleanup();
}
}
else
{
args.Request.Sample = null;
Stop();
Cleanup();
}
}
Im Handler für das Starting-Ereignis rufen wir WaitForNewFrame auf, übergeben jedoch nur die Systemzeit, zu der der Frame an die MediaStreamSourceStartingRequest.SetActualStartPosition-Methode erfasst wurde, die von der MediaStreamSource zum ordnungsgemäßen Codieren der Anzeigedauer der nachfolgenden Frames verwendet wird.
private void OnMediaStreamSourceStarting(MediaStreamSource sender, MediaStreamSourceStartingEventArgs args)
{
using (var frame = WaitForNewFrame())
{
args.Request.SetActualStartPosition(frame.SystemRelativeTime);
}
}
Aufzeichnung starten
Die in diesem Schritt gezeigte StartCapture-Methode wird aus der in einem vorherigen Schritt gezeigten EncodeAsync-Hilfsmethode aufgerufen. Zunächst initialisiert diese Methode eine Reihe von Ereignisobjekten, die zum Steuern des Flows des Aufnahmevorgangs verwendet werden.
- multithread ist eine Hilfsklasse, die das Multithread-Objekt der SharpDX-Bibliothek umschließt und verwendet wird, um sicherzustellen, dass keine anderen Threads auf die SharpDX-Textur zugreifen, während diese kopiert wird.
- frameEvent wird verwendet, um zu signalisieren, dass ein neuer Frame erfasst wurde und an die MediaStreamSource übergeben werden kann
- closedEvent signalisiert, dass die Aufzeichnung beendet wurde und dass wir nicht auf neue Frames warten sollten.
Das Frameereignis und das geschlossene Ereignis werden einem Array hinzugefügt, sodass wir auf eines der Ereignisse in der Aufnahmeschleife warten können.
Der Rest der StartCapture-Methode richtet die Windows.Graphics.Capture-APIs ein, die die eigentliche Bildschirmaufnahme ausführen. Zunächst wird ein Ereignis für das CaptureItem.Closed-Ereignis registriert. Als Nächstes wird ein Direct3D11CaptureFramePool erstellt, mit dem mehrere erfasste Frames gleichzeitig gepuffert werden können. Die CreateFreeThreaded-Methode wird verwendet, um den Framepool zu erstellen, sodass das FrameArrived-Ereignis im eigenen Arbeitsthread des Pools und nicht im Hauptthread der App aufgerufen wird. Als Nächstes wird ein Handler für das FrameArrived-Ereignis registriert. Schließlich wird eine GraphicsCaptureSession für das ausgewählte CaptureItem erstellt, und die Aufnahme von Frames wird durch Aufrufen von StartCapture initiiert.
public void StartCapture()
{
_multithread = _sharpDxD3dDevice.QueryInterface<SharpDX.Direct3D11.Multithread>();
_multithread.SetMultithreadProtected(true);
_frameEvent = new ManualResetEvent(false);
_closedEvent = new ManualResetEvent(false);
_events = new[] { _closedEvent, _frameEvent };
_captureItem.Closed += OnClosed;
_framePool = Direct3D11CaptureFramePool.CreateFreeThreaded(
_device,
DirectXPixelFormat.B8G8R8A8UIntNormalized,
1,
_captureItem.Size);
_framePool.FrameArrived += OnFrameArrived;
_session = _framePool.CreateCaptureSession(_captureItem);
_session.StartCapture();
}
Behandeln von Grafikerfassungsereignissen
Im vorherigen Schritt haben wir zwei Handler für Grafikerfassungsereignisse registriert und einige Ereignisse eingerichtet, um den Flow der Aufnahmeschleife zu verwalten.
Das FrameArrived-Ereignis wird ausgelöst, wenn der Direct3D11CaptureFramePool einen neuen erfassten Frame verfügbar hat. Rufen Sie im Handler für dieses Ereignis TryGetNextFrame für den Sender auf, um den nächsten erfassten Frame abzurufen. Nachdem der Frame abgerufen wurde, legen wir _frameEvent so fest, dass unsere Aufnahmeschleife weiß, dass ein neuer Frame verfügbar ist.
private void OnFrameArrived(Direct3D11CaptureFramePool sender, object args)
{
_currentFrame = sender.TryGetNextFrame();
_frameEvent.Set();
}
Im Closed-Ereignishandler signalisieren wir den _closedEvent, damit die Aufnahmeschleife weiß, wann sie beendet werden soll.
private void OnClosed(GraphicsCaptureItem sender, object args)
{
_closedEvent.Set();
}
Warten auf neue Frames
Die in diesem Abschnitt beschriebene Hilfsmethode WaitForNewFrame ist die Hauptaufgabe der Erfassungsschleife. Denken Sie daran, dass diese Methode vom OnMediaStreamSourceSampleRequested-Ereignishandler aufgerufen wird, wenn die MediaStreamSource bereit ist, einen neuen Frame zum Videostream hinzuzufügen. Auf hoher Ebene kopiert diese Funktion einfach jeden erfassten Videoframe von einer Direct3D-Oberfläche in eine andere, sodass sie zur Codierung an die MediaStreamSource übergeben werden kann, während ein neuer Frame erfasst wird. In diesem Beispiel wird die SharpDX-Bibliothek verwendet, um den tatsächlichen Kopiervorgang auszuführen.
Bevor sie auf einen neuen Frame wartet, verwirft die Methode alle vorherigen Frames, die in der Klassenvariablen_currentFrame gespeichert sind, und setzt _frameEvent zurück. Anschließend wartet die Methode, bis _frameEvent oder die _closedEvent signalisiert werden. Wenn das geschlossene Ereignis festgelegt ist, ruft die App eine Hilfsmethode auf, um die Aufnahmeressourcen zu säubern. Diese Methode wird weiter unten in diesem Artikel gezeigt.
Wenn das Frameereignis festgelegt ist, wissen wir, dass der im vorherigen Schritt definierte FrameArrived-Ereignishandler aufgerufen wurde, und wir beginnen mit dem Kopieren der erfassten Framedaten in eine Direct3D 11-Oberfläche, die an die MediaStreamSource übergeben wird.
In diesem Beispiel wird eine Hilfsklasse SurfaceWithInfo verwendet, mit der wir einfach den Videoframe und die Systemzeit des Frames übergeben können – beides ist für mediaStreamSource erforderlich – als einzelnes Objekt. Der erste Schritt des Frame-Kopiervorgangs besteht darin, diese Klasse zu instanziieren und die Systemzeit festzulegen.
Die nächsten Schritte sind Teil dieses Beispiels, das speziell auf der SharpDX-Bibliothek basiert. Die hier verwendeten Hilfsfunktionen werden am Ende dieses Artikels definiert. Zuerst verwenden wir MultiThreadLock, um sicherzustellen, dass während der Erstellung der Kopie keine anderen Threads auf den Videoframepuffer zugreifen. Als Nächstes rufen wir die Hilfsmethode CreateSharpDXTexture2D auf, um ein SharpDX Texture2D-Objekt aus dem Videoframe zu erstellen. Dies ist die Quelltextur für den Kopiervorgang.
Als Nächstes kopieren wir das im vorherigen Schritt erstellte Texture2D-Objekt in die Kompositionstextur, die wir zuvor im Prozess erstellt haben. Diese Kompositionstextur fungiert als Swappuffer, sodass der Codierungsprozess auf den Pixeln ausgeführt werden kann, während der nächste Frame erfasst wird. Zum Ausführen der Kopie löschen wir die mit der Kompositionstextur verknüpfte Renderzielansicht, definieren dann den Bereich innerhalb der Textur, die wir kopieren möchten – die gesamte Textur in diesem Fall, und dann rufen wir CopySubresourceRegion auf, um die Pixel tatsächlich in die Kompositionstextur zu kopieren.
Wir erstellen eine Kopie der Texturbeschreibung, die verwendet werden soll, wenn wir unsere Zieltextur erstellen, aber die Beschreibung wird geändert, indem die BindFlags auf RenderTarget festgelegt werden, sodass die neue Textur Schreibzugriff hat. Durch Festlegen von CpuAccessFlags auf None kann das System den Kopiervorgang optimieren. Die Texturbeschreibung wird verwendet, um eine neue Texturressource zu erstellen, und die Texturressource der Komposition wird mit einem Aufruf von CopyResource in diese neue Ressource kopiert. Schließlich wird CreateDirect3DSurfaceFromSharpDXTexture aufgerufen, um das von dieser Methode zurückgegebene IDirect3DSurface-Objekt zu erstellen.
public SurfaceWithInfo WaitForNewFrame()
{
// Let's get a fresh one.
_currentFrame?.Dispose();
_frameEvent.Reset();
var signaledEvent = _events[WaitHandle.WaitAny(_events)];
if (signaledEvent == _closedEvent)
{
Cleanup();
return null;
}
var result = new SurfaceWithInfo();
result.SystemRelativeTime = _currentFrame.SystemRelativeTime;
using (var multithreadLock = new MultithreadLock(_multithread))
using (var sourceTexture = Direct3D11Helpers.CreateSharpDXTexture2D(_currentFrame.Surface))
{
_sharpDxD3dDevice.ImmediateContext.ClearRenderTargetView(_composeRenderTargetView, new SharpDX.Mathematics.Interop.RawColor4(0, 0, 0, 1));
var width = Math.Clamp(_currentFrame.ContentSize.Width, 0, _currentFrame.Surface.Description.Width);
var height = Math.Clamp(_currentFrame.ContentSize.Height, 0, _currentFrame.Surface.Description.Height);
var region = new SharpDX.Direct3D11.ResourceRegion(0, 0, 0, width, height, 1);
_sharpDxD3dDevice.ImmediateContext.CopySubresourceRegion(sourceTexture, 0, region, _composeTexture, 0);
var description = sourceTexture.Description;
description.Usage = SharpDX.Direct3D11.ResourceUsage.Default;
description.BindFlags = SharpDX.Direct3D11.BindFlags.ShaderResource | SharpDX.Direct3D11.BindFlags.RenderTarget;
description.CpuAccessFlags = SharpDX.Direct3D11.CpuAccessFlags.None;
description.OptionFlags = SharpDX.Direct3D11.ResourceOptionFlags.None;
using (var copyTexture = new SharpDX.Direct3D11.Texture2D(_sharpDxD3dDevice, description))
{
_sharpDxD3dDevice.ImmediateContext.CopyResource(_composeTexture, copyTexture);
result.Surface = Direct3D11Helpers.CreateDirect3DSurfaceFromSharpDXTexture(copyTexture);
}
}
return result;
}
Beenden der Erfassung und Säubern von Ressourcen
Die Stop-Methode bietet eine Möglichkeit, den Aufnahmevorgang zu beenden. Ihre App kann dies programmgesteuert oder als Reaktion auf eine Benutzerinteraktion aufrufen, z. B. durch Klicken auf eine Schaltfläche. Mit dieser Methode wird einfach die _closedEvent festgelegt. Die in den vorherigen Schritten definierte WaitForNewFrame-Methode sucht nach diesem Ereignis und wird, falls festgelegt, den Aufnahmevorgang herunterfahren.
private void Stop()
{
_closedEvent.Set();
}
Die Cleanup-Methode wird verwendet, um die Ressourcen, die während des Kopiervorgangs erstellt wurden, ordnungsgemäß zu löschen. Dies umfasst:
- Das von der Erfassungssitzung verwendete Direct3D11CaptureFramePool-Objekt
- Die GraphicsCaptureSession und GraphicsCaptureItem
- Die Direct3D- und SharpDX-Geräte
- Die SharpDX-Textur und die Renderzielansicht, die im Kopiervorgang verwendet wird.
- Der Direct3D11CaptureFrame, der zum Speichern des aktuellen Frames verwendet wird.
private void Cleanup()
{
_framePool?.Dispose();
_session?.Dispose();
if (_captureItem != null)
{
_captureItem.Closed -= OnClosed;
}
_captureItem = null;
_device = null;
_sharpDxD3dDevice = null;
_composeTexture?.Dispose();
_composeTexture = null;
_composeRenderTargetView?.Dispose();
_composeRenderTargetView = null;
_currentFrame?.Dispose();
}
Hilfs-Wrapper-Klassen
Die folgenden Hilfsklassen wurden definiert, um dem Beispielcode in diesem Artikel zu helfen.
Die MultithreadLock-Hilfsklasse umschließt die SharpDX Multithread-Klasse, die sicherstellt, dass andere Threads nicht auf die Texturressourcen zugreifen, während sie kopiert werden.
class MultithreadLock : IDisposable
{
public MultithreadLock(SharpDX.Direct3D11.Multithread multithread)
{
_multithread = multithread;
_multithread?.Enter();
}
public void Dispose()
{
_multithread?.Leave();
_multithread = null;
}
private SharpDX.Direct3D11.Multithread _multithread;
}
SurfaceWithInfo wird verwendet, um eine IDirect3DSurface einem SystemRelativeTime-Objekt zuzuordnen, das einen erfassten Frame bzw. die Uhrzeit der Aufnahme darstellt.
public sealed class SurfaceWithInfo : IDisposable
{
public IDirect3DSurface Surface { get; internal set; }
public TimeSpan SystemRelativeTime { get; internal set; }
public void Dispose()
{
Surface?.Dispose();
Surface = null;
}
}
Direct3D- und SharpDX-Hilfs-APIs
Die folgenden Hilfs-APIs sind definiert, um die Erstellung von Direct3D- und SharpDX-Ressourcen zu abstrahieren. Eine ausführliche Erläuterung dieser Technologien liegt außerhalb des Umfangs dieses Artikels, aber der Code wird hier bereitgestellt, damit Sie den in der exemplarischen Vorgehensweise gezeigten Beispielcode implementieren können.
[ComImport]
[Guid("A9B3D012-3DF2-4EE3-B8D1-8695F457D3C1")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[ComVisible(true)]
interface IDirect3DDxgiInterfaceAccess
{
IntPtr GetInterface([In] ref Guid iid);
};
public static class Direct3D11Helpers
{
internal static Guid IInspectable = new Guid("AF86E2E0-B12D-4c6a-9C5A-D7AA65101E90");
internal static Guid ID3D11Resource = new Guid("dc8e63f3-d12b-4952-b47b-5e45026a862d");
internal static Guid IDXGIAdapter3 = new Guid("645967A4-1392-4310-A798-8053CE3E93FD");
internal static Guid ID3D11Device = new Guid("db6f6ddb-ac77-4e88-8253-819df9bbf140");
internal static Guid ID3D11Texture2D = new Guid("6f15aaf2-d208-4e89-9ab4-489535d34f9c");
[DllImport(
"d3d11.dll",
EntryPoint = "CreateDirect3D11DeviceFromDXGIDevice",
SetLastError = true,
CharSet = CharSet.Unicode,
ExactSpelling = true,
CallingConvention = CallingConvention.StdCall
)]
internal static extern UInt32 CreateDirect3D11DeviceFromDXGIDevice(IntPtr dxgiDevice, out IntPtr graphicsDevice);
[DllImport(
"d3d11.dll",
EntryPoint = "CreateDirect3D11SurfaceFromDXGISurface",
SetLastError = true,
CharSet = CharSet.Unicode,
ExactSpelling = true,
CallingConvention = CallingConvention.StdCall
)]
internal static extern UInt32 CreateDirect3D11SurfaceFromDXGISurface(IntPtr dxgiSurface, out IntPtr graphicsSurface);
public static IDirect3DDevice CreateD3DDevice()
{
return CreateD3DDevice(false);
}
public static IDirect3DDevice CreateD3DDevice(bool useWARP)
{
var d3dDevice = new SharpDX.Direct3D11.Device(
useWARP ? SharpDX.Direct3D.DriverType.Software : SharpDX.Direct3D.DriverType.Hardware,
SharpDX.Direct3D11.DeviceCreationFlags.BgraSupport);
IDirect3DDevice device = null;
// Acquire the DXGI interface for the Direct3D device.
using (var dxgiDevice = d3dDevice.QueryInterface<SharpDX.DXGI.Device3>())
{
// Wrap the native device using a WinRT interop object.
uint hr = CreateDirect3D11DeviceFromDXGIDevice(dxgiDevice.NativePointer, out IntPtr pUnknown);
if (hr == 0)
{
device = Marshal.GetObjectForIUnknown(pUnknown) as IDirect3DDevice;
Marshal.Release(pUnknown);
}
}
return device;
}
internal static IDirect3DSurface CreateDirect3DSurfaceFromSharpDXTexture(SharpDX.Direct3D11.Texture2D texture)
{
IDirect3DSurface surface = null;
// Acquire the DXGI interface for the Direct3D surface.
using (var dxgiSurface = texture.QueryInterface<SharpDX.DXGI.Surface>())
{
// Wrap the native device using a WinRT interop object.
uint hr = CreateDirect3D11SurfaceFromDXGISurface(dxgiSurface.NativePointer, out IntPtr pUnknown);
if (hr == 0)
{
surface = Marshal.GetObjectForIUnknown(pUnknown) as IDirect3DSurface;
Marshal.Release(pUnknown);
}
}
return surface;
}
internal static SharpDX.Direct3D11.Device CreateSharpDXDevice(IDirect3DDevice device)
{
var access = (IDirect3DDxgiInterfaceAccess)device;
var d3dPointer = access.GetInterface(ID3D11Device);
var d3dDevice = new SharpDX.Direct3D11.Device(d3dPointer);
return d3dDevice;
}
internal static SharpDX.Direct3D11.Texture2D CreateSharpDXTexture2D(IDirect3DSurface surface)
{
var access = (IDirect3DDxgiInterfaceAccess)surface;
var d3dPointer = access.GetInterface(ID3D11Texture2D);
var d3dSurface = new SharpDX.Direct3D11.Texture2D(d3dPointer);
return d3dSurface;
}
public static SharpDX.Direct3D11.Texture2D InitializeComposeTexture(
SharpDX.Direct3D11.Device sharpDxD3dDevice,
SizeInt32 size)
{
var description = new SharpDX.Direct3D11.Texture2DDescription
{
Width = size.Width,
Height = size.Height,
MipLevels = 1,
ArraySize = 1,
Format = SharpDX.DXGI.Format.B8G8R8A8_UNorm,
SampleDescription = new SharpDX.DXGI.SampleDescription()
{
Count = 1,
Quality = 0
},
Usage = SharpDX.Direct3D11.ResourceUsage.Default,
BindFlags = SharpDX.Direct3D11.BindFlags.ShaderResource | SharpDX.Direct3D11.BindFlags.RenderTarget,
CpuAccessFlags = SharpDX.Direct3D11.CpuAccessFlags.None,
OptionFlags = SharpDX.Direct3D11.ResourceOptionFlags.None
};
var composeTexture = new SharpDX.Direct3D11.Texture2D(sharpDxD3dDevice, description);
using (var renderTargetView = new SharpDX.Direct3D11.RenderTargetView(sharpDxD3dDevice, composeTexture))
{
sharpDxD3dDevice.ImmediateContext.ClearRenderTargetView(renderTargetView, new SharpDX.Mathematics.Interop.RawColor4(0, 0, 0, 1));
}
return composeTexture;
}
}
Siehe auch
- Windows.Graphics.Capture-Namespace
- Bildschirmaufnahme