diff --git a/FlyleafLib/MediaFramework/MediaRenderer/ChildRenderer.cs b/FlyleafLib/MediaFramework/MediaRenderer/ChildRenderer.cs new file mode 100644 index 00000000..a6d5d67a --- /dev/null +++ b/FlyleafLib/MediaFramework/MediaRenderer/ChildRenderer.cs @@ -0,0 +1,443 @@ +using System.Windows; + +using Vortice; +using Vortice.Direct3D11; +using Vortice.DirectComposition; +using Vortice.DXGI; +using Vortice.Mathematics; + +using FlyleafLib.MediaFramework.MediaFrame; + +using static FlyleafLib.Utils.NativeMethods; + +using ID3D11Texture2D = Vortice.Direct3D11.ID3D11Texture2D; + +using FlyleafLib; + +namespace FlyleafLib.MediaFramework.MediaRenderer; + +/// +/// Child renderer that shares the D3D11 device with the main renderer and presents frames to its own swap chain +/// Supports individual pan, zoom, and rotation settings without affecting the main renderer +/// +public class ChildRenderer : IDisposable +{ + #region Properties + public int UniqueId { get; private set; } + public int ControlWidth { get; private set; } + public int ControlHeight { get; private set; } + public bool Disposed { get; private set; } = true; + public bool IsActive { get; private set; } + + public Viewport GetViewport { get; private set; } + public event EventHandler ViewportChanged; + + // Pan/Zoom/Rotation support (independent from main renderer) + public int PanXOffset { get => panXOffset; set => SetPanX(value); } + int panXOffset; + + public int PanYOffset { get => panYOffset; set => SetPanY(value); } + int panYOffset; + + public double Zoom { get => zoom; set => SetZoom(value); } + double zoom = 1; + + public Point ZoomCenter { get => zoomCenter; set => SetZoomCenter(value); } + Point zoomCenter = new(0.5, 0.5); + + public uint Rotation { get => rotation; set => UpdateRotation(value); } + uint rotation; + + public bool HFlip { get => hFlip; set { hFlip = value; UpdateRotation(rotation); } } + bool hFlip; + + public bool VFlip { get => vFlip; set { vFlip = value; UpdateRotation(rotation); } } + bool vFlip; + + public CornerRadius CornerRadius { get => cornerRadius; set => UpdateCornerRadius(value); } + CornerRadius cornerRadius = new(0); + #endregion + + #region Internal Fields + internal nint ControlHandle; + internal Renderer ParentRenderer; + + IDXGISwapChain1 swapChain; + ID3D11Texture2D backBuffer; + ID3D11RenderTargetView backBufferRtv; + IDCompositionDevice dCompDevice; + IDCompositionVisual dCompVisual; + IDCompositionTarget dCompTarget; + + volatile bool canRenderPresent; + bool needsResize; + bool needsViewport; + + object lockRender = new(); + + // For viewport calculation + uint visibleWidth; + uint visibleHeight; + double curRatio, keepRatio, fillRatio; + int sideXPixels; + int sideYPixels; + uint textWidth, textHeight; + CropRect cropRect; + VideoProcessorRotation d3d11vpRotation = VideoProcessorRotation.Identity; + #endregion + + public ChildRenderer(Renderer parentRenderer, nint handle, int uniqueId = -1) + { + ParentRenderer = parentRenderer; + ControlHandle = handle; + UniqueId = uniqueId == -1 ? GetUniqueId() : uniqueId; + } + + static int curUniqueId; + static int GetUniqueId() => curUniqueId++; + + public void Initialize() + { + lock (lockRender) + { + if (!Disposed) + Dispose(); + + Disposed = false; + + RECT rect = new(); + GetWindowRect(ControlHandle, ref rect); + ControlWidth = rect.Right - rect.Left; + ControlHeight = rect.Bottom - rect.Top; + + try + { + var swapChainDesc = GetSwapChainDesc(2, 2); + swapChain = Engine.Video.Factory.CreateSwapChainForComposition(ParentRenderer.Device, swapChainDesc); + + DComp.DCompositionCreateDevice(ParentRenderer.dxgiDevice, out dCompDevice).CheckError(); + dCompDevice.CreateTargetForHwnd(ControlHandle, false, out dCompTarget).CheckError(); + dCompDevice.CreateVisual(out dCompVisual).CheckError(); + dCompVisual.SetContent(swapChain).CheckError(); + dCompTarget.SetRoot(dCompVisual).CheckError(); + dCompDevice.Commit().CheckError(); + + backBuffer = swapChain.GetBuffer(0); + backBufferRtv = ParentRenderer.Device.CreateRenderTargetView(backBuffer); + + Engine.Video.Factory.MakeWindowAssociation(ControlHandle, WindowAssociationFlags.IgnoreAll); + + IsActive = true; + canRenderPresent = true; + needsResize = true; + } + catch (Exception e) + { + ParentRenderer.Log?.Error($"[ChildRenderer #{UniqueId}] Initialization failed: {e.Message}"); + Dispose(); + throw; + } + } + } + + private SwapChainDescription1 GetSwapChainDesc(int width, int height) + => new() + { + BufferUsage = Usage.RenderTargetOutput, + Format = ParentRenderer.Config.Video.Swap10Bit ? Format.R10G10B10A2_UNorm : (ParentRenderer.Config.Video.SwapForceR8G8B8A8 ? Format.R8G8B8A8_UNorm : Format.B8G8R8A8_UNorm), + Width = (uint)width, + Height = (uint)height, + AlphaMode = AlphaMode.Premultiplied, + SwapEffect = SwapEffect.FlipDiscard, + Scaling = Scaling.Stretch, + BufferCount = Math.Max(ParentRenderer.Config.Video.SwapBuffers, 2), + SampleDescription = new SampleDescription(1, 0), + Flags = SwapChainFlags.None + }; + + public void Resize(int width, int height) + { + lock (lockRender) + { + if (Disposed || !IsActive) + return; + + if (width == ControlWidth && height == ControlHeight) + return; + + ControlWidth = width; + ControlHeight = height; + needsResize = true; + canRenderPresent = width > 0 && height > 0; + } + } + + private void ResizeBuffersInternal() + { + if (!needsResize) + return; + + needsResize = false; + + if (ControlWidth <= 0 || ControlHeight <= 0) + { + canRenderPresent = false; + return; + } + + backBufferRtv?.Dispose(); + backBuffer?.Dispose(); + + swapChain.ResizeBuffers(0, (uint)ControlWidth, (uint)ControlHeight, Format.Unknown, SwapChainFlags.None).CheckError(); + + backBuffer = swapChain.GetBuffer(0); + backBufferRtv = ParentRenderer.Device.CreateRenderTargetView(backBuffer); + + canRenderPresent = true; + needsViewport = true; + } + + #region Pan/Zoom/Rotation Methods + public void SetPanX(int panX, bool refresh = true) + { + panXOffset = panX; + SetViewport(refresh); + } + + public void SetPanY(int panY, bool refresh = true) + { + panYOffset = panY; + SetViewport(refresh); + } + + public void SetZoom(double zoom, bool refresh = true) + { + this.zoom = zoom; + SetViewport(refresh); + } + + public void SetZoomCenter(Point p, bool refresh = true) + { + zoomCenter = p; + SetViewport(refresh); + } + + public void SetZoomAndCenter(double zoom, Point p, bool refresh = true) + { + this.zoom = zoom; + zoomCenter = p; + SetViewport(refresh); + } + + public void SetPanAll(int panX, int panY, uint rotation, double zoom, Point p, bool refresh = true) + { + panXOffset = panX; + panYOffset = panY; + this.zoom = zoom; + zoomCenter = p; + UpdateRotation(rotation, false); + SetViewport(refresh); + } + + private void UpdateRotation(uint angle, bool refresh = true) + { + rotation = angle; + + // Map rotation to D3D11 VideoProcessor rotation + d3d11vpRotation = (rotation, hFlip, vFlip) switch + { + (0, false, false) => VideoProcessorRotation.Identity, + (90, false, false) => VideoProcessorRotation.Rotation90, + (180, false, false) => VideoProcessorRotation.Rotation180, + (270, false, false) => VideoProcessorRotation.Rotation270, + _ => VideoProcessorRotation.Identity + }; + + if (refresh) + SetViewport(); + } + + private void UpdateCornerRadius(CornerRadius value) + { + cornerRadius = value; + // Corner radius implementation would go here + } + + public void SetViewport(bool refresh = true) + { + lock (lockRender) + { + if (Disposed || !IsActive) + return; + + needsViewport = true; + } + } + + private void SetViewportInternal() + { + if (!needsViewport) + return; + + needsViewport = false; + + // Get video dimensions from parent renderer + visibleWidth = ParentRenderer.VisibleWidth; + visibleHeight = ParentRenderer.VisibleHeight; + + if (visibleWidth == 0 || visibleHeight == 0) + { + GetViewport = new Viewport(0, 0, ControlWidth, ControlHeight); + ViewportChanged?.Invoke(this, EventArgs.Empty); + return; + } + + // Calculate aspect ratios + curRatio = (double)visibleWidth / visibleHeight; + keepRatio = (double)ControlWidth / ControlHeight; + fillRatio = keepRatio; + + int x, y, newWidth, newHeight, xZoomPixels, yZoomPixels; + + if (curRatio < fillRatio) + { + newHeight = ControlHeight; + newWidth = (int)(visibleWidth * ControlHeight / visibleHeight); + sideXPixels = ControlWidth - newWidth; + sideYPixels = 0; + } + else + { + newWidth = ControlWidth; + newHeight = (int)(visibleHeight * ControlWidth / visibleWidth); + sideXPixels = 0; + sideYPixels = ControlHeight - newHeight; + } + + // Apply zoom + xZoomPixels = (int)((newWidth * zoom) - newWidth); + yZoomPixels = (int)((newHeight * zoom) - newHeight); + + newWidth += xZoomPixels; + newHeight += yZoomPixels; + + // Apply zoom center + x = (int)((sideXPixels / 2) - (xZoomPixels * zoomCenter.X)); + y = (int)((sideYPixels / 2) - (yZoomPixels * zoomCenter.Y)); + + // Apply pan offsets + x += panXOffset; + y += panYOffset; + + GetViewport = new Viewport(x, y, newWidth, newHeight); + ViewportChanged?.Invoke(this, EventArgs.Empty); + } + #endregion + + /// + /// Renders the current frame from the parent renderer to this child's swap chain + /// Should be called by the parent renderer during its render loop + /// + internal void RenderFrame(VideoFrame frame, bool secondField) + { + lock (lockRender) + { + if (Disposed || !IsActive || !canRenderPresent) + return; + + try + { + ResizeBuffersInternal(); + SetViewportInternal(); + + if (!canRenderPresent) + return; + + var context = ParentRenderer.context; + + // Set our render target + context.OMSetRenderTargets(backBufferRtv); + context.ClearRenderTargetView(backBufferRtv, ParentRenderer.Config.Video._BackgroundColor); + + // Set our viewport + context.RSSetViewport(GetViewport); + + // If frame uses Flyleaf shaders (has shader resource views) + if (frame.srvs != null) + { + // Use shader resource views directly - this is the efficient path + context.PSSetShaderResources(0, frame.srvs); + context.Draw(6, 0); + } + else + { + // For D3D11 Video Processor output, we need the parent to render to backBuffer first + // Then we copy from parent's backBuffer to ours + // This is less efficient but necessary for D3D11VP path + // The parent renderer will already have rendered to its backBuffer + // We just need to copy it with our viewport + + // Note: The parent's backBuffer should already have the rendered frame + // We'll create an SRV from parent's backBuffer and render it to ours + using var parentBackBufferSrv = ParentRenderer.Device.CreateShaderResourceView(ParentRenderer.backBuffer); + context.PSSetShaderResources(0, new[] { parentBackBufferSrv }); + context.Draw(6, 0); + } + + // Restore parent's viewport + context.RSSetViewport(ParentRenderer.GetViewport); + } + catch (Exception e) + { + ParentRenderer.Log?.Error($"[ChildRenderer #{UniqueId}] RenderFrame failed: {e.Message}"); + } + } + } + + /// + /// Presents the rendered frame to the screen + /// Should be called by the parent renderer after all child renderers have rendered + /// + internal void Present() + { + lock (lockRender) + { + if (Disposed || !IsActive || !canRenderPresent) + return; + + try + { + swapChain.Present(ParentRenderer.Config.Video.VSync, PresentFlags.None).CheckError(); + } + catch (Exception e) + { + ParentRenderer.Log?.Error($"[ChildRenderer #{UniqueId}] Present failed: {e.Message}"); + } + } + } + + public void Dispose() + { + lock (lockRender) + { + if (Disposed) + return; + + Disposed = true; + IsActive = false; + + backBufferRtv?.Dispose(); + backBuffer?.Dispose(); + swapChain?.Dispose(); + + if (dCompDevice != null) + { + dCompDevice.Dispose(); + dCompDevice = null; + } + + dCompVisual?.Dispose(); + dCompTarget?.Dispose(); + } + } +} diff --git a/FlyleafLib/MediaFramework/MediaRenderer/ChildRenderers.README.md b/FlyleafLib/MediaFramework/MediaRenderer/ChildRenderers.README.md new file mode 100644 index 00000000..c463fab0 --- /dev/null +++ b/FlyleafLib/MediaFramework/MediaRenderer/ChildRenderers.README.md @@ -0,0 +1,199 @@ +# Child Renderers Support + +This feature allows displaying the same video stream in multiple host controls simultaneously, useful for scenarios like: +- Displaying multiple video streams as thumbnails with a selected thumbnail shown in a larger view +- Picture-in-picture implementations +- Multi-monitor setups with synchronized playback + +## Architecture + +The implementation uses **child swap chains** that share the same D3D11 device with the main renderer. This approach: +- Avoids copying frames between devices (efficient) +- Requires all surfaces to be on the same GPU adapter +- Supports independent pan, zoom, and rotation on each child renderer +- Child renderers are not full players (no separate audio output) + +## Key Components + +### 1. ChildRenderer Class +Located in `FlyleafLib/MediaFramework/MediaRenderer/ChildRenderer.cs` + +A child renderer manages its own swap chain and can render frames from the parent renderer with independent viewing settings: +- Pan (X/Y offsets) +- Zoom (scale factor and center point) +- Rotation (0°, 90°, 180°, 270°) +- Horizontal/Vertical flip + +### 2. Renderer.ChildRenderers.cs +Extension to the main Renderer class that provides: +- `RegisterChildRenderer()` - Registers a child renderer +- `UnregisterChildRenderer()` - Unregisters a child renderer +- `GetChildRenderers()` - Gets all registered child renderers +- Internal methods for rendering and presenting to child renderers + +### 3. Player.ChildRenderers.cs +Player-level API for easier child renderer management: +- `CreateChildRenderer()` - Creates and registers a new child renderer +- `RemoveChildRenderer()` - Removes and disposes a child renderer +- `GetChildRenderers()` - Gets all child renderers + +## Usage Example (WPF) + +### Basic Setup + +```csharp +using FlyleafLib; +using FlyleafLib.MediaPlayer; +using FlyleafLib.MediaFramework.MediaRenderer; + +public partial class MainWindow : Window +{ + private Player player; + private ChildRenderer thumbnailRenderer; + + public MainWindow() + { + InitializeComponent(); + + // Initialize player + Engine.Start(new EngineConfig()); + player = new Player(new Config()); + + // Attach player to main view + mainVideoHost.Player = player; + + // Create child renderer for thumbnail view + // thumbnailHost is another FlyleafHost control + var thumbnailHandle = new WindowInteropHelper(thumbnailHost.Surface).Handle; + thumbnailRenderer = player.CreateChildRenderer(thumbnailHandle); + + if (thumbnailRenderer != null) + { + // Configure thumbnail with zoom out for overview + thumbnailRenderer.Zoom = 0.5; + } + } + + private void OnThumbnailClick(object sender, EventArgs e) + { + // Can adjust child renderer view independently + thumbnailRenderer.Zoom = 1.0; + thumbnailRenderer.SetPanAll(0, 0, 0, 1.0, new Point(0.5, 0.5)); + } + + protected override void OnClosed(EventArgs e) + { + thumbnailRenderer?.Dispose(); + player?.Dispose(); + base.OnClosed(e); + } +} +``` + +### Multiple Thumbnails + +```csharp +private List thumbnailRenderers = new(); + +private void CreateThumbnailGrid() +{ + foreach (var thumbnailHost in thumbnailHostsGrid.Children.OfType()) + { + var handle = new WindowInteropHelper(thumbnailHost.Surface).Handle; + var childRenderer = player.CreateChildRenderer(handle); + + if (childRenderer != null) + { + // Configure each thumbnail independently + childRenderer.Zoom = 0.3; + thumbnailRenderers.Add(childRenderer); + } + } +} + +private void CleanupThumbnails() +{ + foreach (var renderer in thumbnailRenderers) + { + player.RemoveChildRenderer(renderer); + } + thumbnailRenderers.Clear(); +} +``` + +### Dynamic Pan/Zoom on Child Renderer + +```csharp +// Pan the child renderer view +thumbnailRenderer.PanXOffset = 100; // Pan right by 100 pixels +thumbnailRenderer.PanYOffset = -50; // Pan up by 50 pixels + +// Zoom with center point +thumbnailRenderer.SetZoomAndCenter(2.0, new Point(0.3, 0.3)); + +// Zoom to specific point maintaining that point's position +Point clickPoint = new Point(200, 150); +thumbnailRenderer.ZoomWithCenterPoint(clickPoint, 1.5); + +// Rotate +thumbnailRenderer.Rotation = 90; // 0, 90, 180, or 270 + +// Flip +thumbnailRenderer.HFlip = true; +thumbnailRenderer.VFlip = false; + +// Reset all transformations +thumbnailRenderer.SetPanAll(0, 0, 0, 1.0, new Point(0.5, 0.5)); +``` + +## Performance Considerations + +1. **Shared Device**: All child renderers share the same D3D11 device as the main renderer + - Must be on the same GPU adapter + - Cross-adapter scenarios will require additional copying (not currently implemented) + +2. **Video Processor Path**: + - For D3D11 Video Processor: Child renderers copy from parent's backBuffer + - For Flyleaf Shaders: Child renderers use frame's SRVs directly (more efficient) + +3. **Synchronization**: Child renderers render and present synchronously with the main renderer + - All rendering happens on the same thread + - Present operations are sequential + +## Limitations + +1. Child renderers are **not independent players**: + - No separate audio output + - No separate playback controls + - No separate subtitle rendering (currently) + +2. GPU adapter constraints: + - All surfaces must be on the same GPU adapter + - Moving windows to different monitors on different GPUs is not supported + +3. No separate configuration: + - VSync and buffer settings are inherited from parent renderer + +## Future Enhancements + +Possible future improvements: +- Support for subtitle rendering on child renderers +- Support for overlay content on child renderers +- Cross-adapter rendering with automatic texture copying +- Asynchronous rendering option for better performance +- Independent VSync settings per child renderer + +## Thread Safety + +All child renderer operations are thread-safe: +- Registration/unregistration is protected by locks +- Rendering operations are synchronized with the main renderer +- Disposal is safe to call from any thread + +## Best Practices + +1. **Initialize after player is ready**: Create child renderers after the player has opened a video stream +2. **Dispose properly**: Always dispose child renderers before disposing the player +3. **Handle window resize**: Call `childRenderer.Resize(width, height)` when the window size changes +4. **Check IsActive**: Verify `childRenderer.IsActive` before performing operations +5. **Handle failures**: Check for null returns from `CreateChildRenderer()` and handle errors gracefully diff --git a/FlyleafLib/MediaFramework/MediaRenderer/Renderer.ChildRenderers.cs b/FlyleafLib/MediaFramework/MediaRenderer/Renderer.ChildRenderers.cs new file mode 100644 index 00000000..f226c1f6 --- /dev/null +++ b/FlyleafLib/MediaFramework/MediaRenderer/Renderer.ChildRenderers.cs @@ -0,0 +1,142 @@ +using FlyleafLib.MediaFramework.MediaFrame; +using FlyleafLib.Utils; + +namespace FlyleafLib.MediaFramework.MediaRenderer; + +public unsafe partial class Renderer +{ + #region Child Renderers Management + private readonly List childRenderers = new(); + private readonly object lockChildRenderers = new(); + + /// + /// Registers a child renderer that will receive frames from this main renderer + /// The child renderer will share the same D3D11 device + /// + /// The child renderer to register + public void RegisterChildRenderer(ChildRenderer childRenderer) + { + if (childRenderer == null) + throw new ArgumentNullException(nameof(childRenderer)); + + if (childRenderer.ParentRenderer != this) + throw new InvalidOperationException("Child renderer must have this renderer as its parent"); + + lock (lockChildRenderers) + { + if (!childRenderers.Contains(childRenderer)) + { + childRenderers.Add(childRenderer); + if (Logger.CanInfo) Log.Info($"Registered child renderer #{childRenderer.UniqueId}"); + } + } + } + + /// + /// Unregisters a child renderer + /// + /// The child renderer to unregister + public void UnregisterChildRenderer(ChildRenderer childRenderer) + { + if (childRenderer == null) + return; + + lock (lockChildRenderers) + { + if (childRenderers.Remove(childRenderer)) + { + if (Logger.CanInfo) Log.Info($"Unregistered child renderer #{childRenderer.UniqueId}"); + } + } + } + + /// + /// Gets all registered child renderers + /// + public IReadOnlyList GetChildRenderers() + { + lock (lockChildRenderers) + { + return childRenderers.ToList(); + } + } + + /// + /// Renders the current frame to all active child renderers + /// Should be called during the main render loop after rendering to the main swap chain + /// + private void RenderToChildRenderers(VideoFrame frame, bool secondField) + { + lock (lockChildRenderers) + { + if (childRenderers.Count == 0) + return; + + foreach (var child in childRenderers) + { + if (child.IsActive && !child.Disposed) + { + try + { + child.RenderFrame(frame, secondField); + } + catch (Exception e) + { + Log.Error($"Failed to render to child renderer #{child.UniqueId}: {e.Message}"); + } + } + } + } + } + + /// + /// Presents all child renderers + /// Should be called after the main renderer presents + /// + private void PresentChildRenderers() + { + lock (lockChildRenderers) + { + if (childRenderers.Count == 0) + return; + + foreach (var child in childRenderers) + { + if (child.IsActive && !child.Disposed) + { + try + { + child.Present(); + } + catch (Exception e) + { + Log.Error($"Failed to present child renderer #{child.UniqueId}: {e.Message}"); + } + } + } + } + } + + /// + /// Disposes all child renderers + /// + private void DisposeChildRenderers() + { + lock (lockChildRenderers) + { + foreach (var child in childRenderers) + { + try + { + child.Dispose(); + } + catch (Exception e) + { + Log.Error($"Failed to dispose child renderer #{child.UniqueId}: {e.Message}"); + } + } + childRenderers.Clear(); + } + } + #endregion +} diff --git a/FlyleafLib/MediaFramework/MediaRenderer/Renderer.Device.cs b/FlyleafLib/MediaFramework/MediaRenderer/Renderer.Device.cs index 123e97ab..a2425802 100644 --- a/FlyleafLib/MediaFramework/MediaRenderer/Renderer.Device.cs +++ b/FlyleafLib/MediaFramework/MediaRenderer/Renderer.Device.cs @@ -67,14 +67,14 @@ static Renderer() } internal ID3D11Device Device; - IDXGIDevice1 dxgiDevice; + internal IDXGIDevice1 dxgiDevice; public FeatureLevel FeatureLevel { get; private set; } public GPUAdapter GPUAdapter => gpuAdapter; IDXGIAdapter dxgiAdapter; GPUAdapter gpuAdapter; bool gpuForceWarp; - ID3D11DeviceContext context; + internal ID3D11DeviceContext context; ID3D11Buffer vertexBuffer; ID3D11InputLayout inputLayout; @@ -270,6 +270,9 @@ public void Dispose() LastFrame = null; } + // Dispose all child renderers first + DisposeChildRenderers(); + DisposeSwapChain(); if (use2d) diff --git a/FlyleafLib/MediaFramework/MediaRenderer/Renderer.Present.cs b/FlyleafLib/MediaFramework/MediaRenderer/Renderer.Present.cs index 6a0d1dd1..7aad3a1a 100644 --- a/FlyleafLib/MediaFramework/MediaRenderer/Renderer.Present.cs +++ b/FlyleafLib/MediaFramework/MediaRenderer/Renderer.Present.cs @@ -158,8 +158,13 @@ internal bool PresentPlay() try { if (canRenderPresent) + { swapChain.Present(Config.Video.VSync, PresentFlags.None).CheckError(); + // Present child renderers + PresentChildRenderers(); + } + return true; } catch (SharpGenException e) @@ -207,8 +212,13 @@ internal bool RefreshPlay(bool secondField) } if (canRenderPresent) + { swapChain.Present(Config.Video.VSync, PresentFlags.None).CheckError(); + // Present child renderers + PresentChildRenderers(); + } + return true; } catch { return false; } @@ -275,6 +285,9 @@ void RenderFrame(VideoFrame frame, bool secondField) // From Play or Idle (No lo context.OMSetBlendState(null); context.RSSetViewport(GetViewport); } + + // Render to child renderers (they share the same device and context) + RenderToChildRenderers(frame, secondField); } public FrameStatistics GetFrameStatistics() diff --git a/FlyleafLib/MediaFramework/MediaRenderer/Renderer.SwapChain.cs b/FlyleafLib/MediaFramework/MediaRenderer/Renderer.SwapChain.cs index 5ba874dc..f718fd03 100644 --- a/FlyleafLib/MediaFramework/MediaRenderer/Renderer.SwapChain.cs +++ b/FlyleafLib/MediaFramework/MediaRenderer/Renderer.SwapChain.cs @@ -16,7 +16,7 @@ unsafe public partial class Renderer { volatile bool canRenderPresent; // Don't render / present during minimize (or invalid size) - ID3D11Texture2D backBuffer; + internal ID3D11Texture2D backBuffer; ID3D11RenderTargetView backBufferRtv; IDXGISwapChain1 swapChain; IDCompositionDevice dCompDevice; diff --git a/FlyleafLib/MediaPlayer/Player.ChildRenderers.cs b/FlyleafLib/MediaPlayer/Player.ChildRenderers.cs new file mode 100644 index 00000000..a4a8b4ad --- /dev/null +++ b/FlyleafLib/MediaPlayer/Player.ChildRenderers.cs @@ -0,0 +1,75 @@ +using FlyleafLib.MediaFramework.MediaRenderer; + +namespace FlyleafLib.MediaPlayer; + +public unsafe partial class Player +{ + #region Child Renderers + /// + /// Creates a child renderer that shares the D3D11 device with the main renderer + /// The child renderer will display the same video content with independent pan/zoom/rotation settings + /// + /// Window handle for the child renderer's output surface + /// Optional unique identifier for the child renderer + /// The created ChildRenderer instance, or null if no renderer is available + public ChildRenderer CreateChildRenderer(nint handle, int uniqueId = -1) + { + if (renderer == null) + { + Log?.Error("Cannot create child renderer: Main renderer is not initialized"); + return null; + } + + if (handle == nint.Zero) + { + Log?.Error("Cannot create child renderer: Invalid window handle"); + return null; + } + + try + { + var childRenderer = new ChildRenderer(renderer, handle, uniqueId); + childRenderer.Initialize(); + renderer.RegisterChildRenderer(childRenderer); + + Log?.Info($"Created child renderer #{childRenderer.UniqueId}"); + return childRenderer; + } + catch (Exception e) + { + Log?.Error($"Failed to create child renderer: {e.Message}"); + return null; + } + } + + /// + /// Removes and disposes a child renderer + /// + /// The child renderer to remove + public void RemoveChildRenderer(ChildRenderer childRenderer) + { + if (childRenderer == null || renderer == null) + return; + + try + { + renderer.UnregisterChildRenderer(childRenderer); + childRenderer.Dispose(); + Log?.Info($"Removed child renderer #{childRenderer.UniqueId}"); + } + catch (Exception e) + { + Log?.Error($"Failed to remove child renderer: {e.Message}"); + } + } + + /// + /// Gets all registered child renderers + /// + /// Read-only list of child renderers + public IReadOnlyList GetChildRenderers() + { + return renderer?.GetChildRenderers() ?? new List(); + } + #endregion +} diff --git a/Samples/FlyleafPlayer (Child Renderers) (WPF)/App.xaml b/Samples/FlyleafPlayer (Child Renderers) (WPF)/App.xaml new file mode 100644 index 00000000..cee9e27d --- /dev/null +++ b/Samples/FlyleafPlayer (Child Renderers) (WPF)/App.xaml @@ -0,0 +1,8 @@ + + + + + diff --git a/Samples/FlyleafPlayer (Child Renderers) (WPF)/App.xaml.cs b/Samples/FlyleafPlayer (Child Renderers) (WPF)/App.xaml.cs new file mode 100644 index 00000000..0fce44da --- /dev/null +++ b/Samples/FlyleafPlayer (Child Renderers) (WPF)/App.xaml.cs @@ -0,0 +1,8 @@ +using System.Windows; + +namespace FlyleafPlayer.ChildRendererSample +{ + public partial class App : Application + { + } +} diff --git a/Samples/FlyleafPlayer (Child Renderers) (WPF)/FlyleafPlayer (Child Renderers) (WPF).csproj b/Samples/FlyleafPlayer (Child Renderers) (WPF)/FlyleafPlayer (Child Renderers) (WPF).csproj new file mode 100644 index 00000000..d85f8b4e --- /dev/null +++ b/Samples/FlyleafPlayer (Child Renderers) (WPF)/FlyleafPlayer (Child Renderers) (WPF).csproj @@ -0,0 +1,26 @@ + + + + WinExe + net8.0-windows + FlyleafPlayer.ChildRendererSample + true + FlyleafPlayer.ChildRenderers + AnyCPU;x86;x64 + SuRGeoNix © 2025 + SuRGeoNix + + + + + + + + + + + + + + + diff --git a/Samples/FlyleafPlayer (Child Renderers) (WPF)/HOW_TO_RUN.md b/Samples/FlyleafPlayer (Child Renderers) (WPF)/HOW_TO_RUN.md new file mode 100644 index 00000000..17a2ed37 --- /dev/null +++ b/Samples/FlyleafPlayer (Child Renderers) (WPF)/HOW_TO_RUN.md @@ -0,0 +1,119 @@ +# How to Build and Run Without Visual Studio + +## Prerequisites + +1. **Windows 10/11** (required - this uses Windows-specific APIs) +2. **.NET 8 SDK or later** + Download from: https://dotnet.microsoft.com/download + +## Quick Start + +### 1. Install .NET SDK (if not installed) +```powershell +# Download and install from: +https://dotnet.microsoft.com/download/dotnet/8.0 + +# Verify installation +dotnet --version +``` + +### 2. Clone/Copy the Repository to Windows +Transfer this entire `/workspaces/Flyleaf` folder to your Windows machine. + +### 3. Build the Project +Open PowerShell or Command Prompt in the Flyleaf directory: + +```powershell +# Navigate to the solution directory +cd path\to\Flyleaf + +# Restore NuGet packages +dotnet restore FlyleafLib.sln + +# Build the solution +dotnet build FlyleafLib.sln --configuration Release +``` + +### 4. Run the Child Renderers Sample +```powershell +# Navigate to the sample directory +cd "Samples\FlyleafPlayer (Child Renderers) (WPF)" + +# Run the application +dotnet run +``` + +That's it! The application should launch. + +## Alternative: Build Only the Sample + +If the full solution has issues, build just the sample: + +```powershell +cd "Samples\FlyleafPlayer (Child Renderers) (WPF)" +dotnet build +dotnet run +``` + +## Alternative: Using Visual Studio Code + +If you have VS Code on Windows: + +1. Install VS Code: https://code.visualstudio.com/ +2. Install C# extension: https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp +3. Open the Flyleaf folder in VS Code +4. Press `F5` to run + +## Alternative: Using Rider + +JetBrains Rider is another option: +1. Install Rider: https://www.jetbrains.com/rider/ +2. Open `FlyleafLib.sln` +3. Click Run + +## Troubleshooting + +### "SDK not found" +Install .NET SDK from: https://dotnet.microsoft.com/download + +### "Project targets .NET 10" +The sample I created uses .NET 8. If you see errors, check the .csproj file has: +```xml +net8.0-windows +``` + +### "Cannot find FFmpeg" +The application will try to load FFmpeg binaries. Make sure they exist in the `FFmpeg` folder. + +### "Build errors" +Try: +```powershell +dotnet clean +dotnet restore +dotnet build +``` + +## What You'll See + +When running successfully: +- Main window with video player +- Two thumbnail views on the right +- Zoom, rotate, flip controls +- Status bar at bottom + +Open a video file (MP4, MKV, etc.) using the "Open Video" button to see all three views synchronized! + +## Running from Executable + +After building, you can run the .exe directly: +``` +Samples\FlyleafPlayer (Child Renderers) (WPF)\bin\Release\net8.0-windows\FlyleafPlayer.ChildRenderers.exe +``` + +## Still Having Issues? + +Check: +1. You're on Windows (Linux/Mac won't work) +2. .NET 8 SDK is installed +3. You have a display (not running headless) +4. The project built successfully (check for error messages) diff --git a/Samples/FlyleafPlayer (Child Renderers) (WPF)/MainWindow.xaml b/Samples/FlyleafPlayer (Child Renderers) (WPF)/MainWindow.xaml new file mode 100644 index 00000000..811a3f6e --- /dev/null +++ b/Samples/FlyleafPlayer (Child Renderers) (WPF)/MainWindow.xaml @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + +