Skip to content

Commit c334cf8

Browse files
committed
feat: add VideoControl — terminal video playback via FFmpeg
Three render modes: half-block (best color, 2 pixels/cell), ASCII (brightness-to-density characters), and braille (2x4 dots/cell, highest spatial resolution). FFmpeg subprocess pipes raw RGB24 frames; pre-allocated cell buffers avoid per-frame GC allocation. Features: - Play/pause/stop with keyboard (Space/M/L/Esc) and mouse - Auto-show/hide overlay status bar (.WithOverlay()) - Dynamic resize: restarts FFmpeg at new dimensions on window resize - Looping, render mode cycling, frame skipping when behind - Graceful FFmpeg-not-found message with install instructions - Fluent builder: Controls.Video("file.mp4").Fill().WithOverlay() Includes DemoApp integration (Video Player nav item with file picker, falls back to bundled sample.mp4), two sample videos (4s particles + 30s Big Buck Bunny), deterministic test pattern, and full documentation.
1 parent 348921d commit c334cf8

25 files changed

Lines changed: 2145 additions & 0 deletions

Examples/DemoApp/DemoApp.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
<ItemGroup>
1515
<Content Include="sample.png" CopyToOutputDirectory="PreserveNewest" />
16+
<Content Include="sample.mp4" CopyToOutputDirectory="PreserveNewest" />
17+
<Content Include="sample_bunny.mp4" CopyToOutputDirectory="PreserveNewest" />
1618
</ItemGroup>
1719

1820
</Project>

Examples/DemoApp/DemoWindows/LauncherWindow.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ public static Window Create(ConsoleWindowSystem ws)
6767
.AddItem("Notifications", subtitle: "Notification system demo", content: MakeInfoPanel("Notifications"))
6868
.AddItem("System Info", subtitle: "OS & runtime details", content: MakeInfoPanel("System Info"))
6969
.AddItem("Terminal", subtitle: "PTY-backed terminal emulator", content: MakeInfoPanel("Terminal"))
70+
.AddItem("Video Player", subtitle: "Terminal video playback with half-block rendering",
71+
content: MakeInfoPanel("Video Player"))
7072
.AddItem("Welcome Banner", subtitle: "FIGlet ASCII art banner", content: MakeInfoPanel("Welcome Banner")))
7173
.OnSelectedItemChanged((sender, args) =>
7274
{
@@ -245,6 +247,7 @@ private static void LaunchDemo(ConsoleWindowSystem ws, string demoName)
245247
"Welcome Banner" => WelcomeWindow.Create(ws),
246248
"Alpha Blending" => AlphaBlendingDemoWindow.Create(ws),
247249
"Canvas Animations" => OpenCanvasWindows(ws),
250+
"Video Player" => VideoDemoWindow.Create(ws),
248251
_ => (Window?)null
249252
};
250253
}
@@ -768,6 +771,28 @@ private static void LaunchDemo(ConsoleWindowSystem ws, string demoName)
768771
" - Animated pulse panel (sin-wave alpha)",
769772
" - Animated background gradient (checkbox-controlled)",
770773
},
774+
"Video Player" => new List<string>
775+
{
776+
"[bold cyan]Video Player[/]",
777+
"",
778+
"Plays video files in the terminal using three",
779+
"rendering modes. Requires FFmpeg on PATH.",
780+
"",
781+
"[dim]Render Modes:[/]",
782+
" - [white]Half-Block:[/] 2 pixels/cell, best color",
783+
" - [white]ASCII:[/] Brightness-to-density characters",
784+
" - [white]Braille:[/] 2x4 dots/cell, highest resolution",
785+
"",
786+
"[dim]Controls:[/]",
787+
" Space — Play / Pause",
788+
" M — Cycle render mode",
789+
" L — Toggle looping",
790+
" Esc — Stop playback",
791+
"",
792+
"[dim]Formats:[/] MP4, MKV, AVI, WebM, MOV, FLV, WMV",
793+
"",
794+
"[dim]Requires:[/] FFmpeg installed and on system PATH",
795+
},
771796
_ => null
772797
};
773798
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using SharpConsoleUI;
2+
using SharpConsoleUI.Builders;
3+
using SharpConsoleUI.Controls;
4+
using SharpConsoleUI.Dialogs;
5+
using SharpConsoleUI.Video;
6+
7+
namespace DemoApp.DemoWindows;
8+
9+
public static class VideoDemoWindow
10+
{
11+
private const string VideoFilter = "*.mp4;*.mkv;*.avi;*.webm;*.mov;*.flv;*.wmv";
12+
13+
public static Window Create(ConsoleWindowSystem ws)
14+
{
15+
var videoControl = Controls.Video()
16+
.Fill()
17+
.WithLooping()
18+
.WithOverlay()
19+
.Build();
20+
21+
var window = new WindowBuilder(ws)
22+
.WithTitle("Video Player")
23+
.WithSize(82, 30)
24+
.Centered()
25+
.WithColors(Color.White, Color.Black)
26+
.AddControl(videoControl)
27+
.WithAsyncWindowThread(async (win, ct) =>
28+
{
29+
// Open file picker asynchronously
30+
var filePath = await FileDialogs.ShowFilePickerAsync(ws,
31+
startPath: AppContext.BaseDirectory,
32+
filter: VideoFilter);
33+
34+
// Fall back to bundled sample if user cancels file picker
35+
if (string.IsNullOrEmpty(filePath))
36+
{
37+
string samplePath = Path.Combine(AppContext.BaseDirectory, "sample.mp4");
38+
if (File.Exists(samplePath))
39+
filePath = samplePath;
40+
else
41+
{
42+
ws.EnqueueOnUIThread(() => win.TryClose(force: true));
43+
return;
44+
}
45+
}
46+
47+
ws.EnqueueOnUIThread(() =>
48+
{
49+
win.Title = $"Video — {Path.GetFileName(filePath)}";
50+
videoControl.PlayFile(filePath);
51+
});
52+
53+
// Keep thread alive until cancellation (playback runs internally)
54+
try { await Task.Delay(Timeout.Infinite, ct); }
55+
catch (OperationCanceledException) { }
56+
})
57+
.BuildAndShow();
58+
59+
window.OnClosed += (_, _) =>
60+
{
61+
videoControl.Stop();
62+
videoControl.Dispose();
63+
};
64+
65+
return window;
66+
}
67+
}

Examples/DemoApp/sample.mp4

321 KB
Binary file not shown.

Examples/DemoApp/sample_bunny.mp4

3.95 MB
Binary file not shown.

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ schost opens your app in Windows Terminal (or Linux terminal emulator) with cust
122122
| **Navigation** | MenuControl, ToolbarControl, TabControl, NavigationView |
123123
| **Layout** | ColumnContainer, SplitterControl, ScrollablePanelControl, PanelControl |
124124
| **Drawing** | CanvasControl, ImageControl (PNG/JPEG/BMP/GIF/WebP/TIFF via ImageSharp) |
125+
| **Video** | VideoControl — terminal video playback via FFmpeg (half-block, ASCII, braille modes) |
125126
| **Advanced** | SpectreRenderableControl (wraps any Spectre.Console `IRenderable`), ProgressBarControl, TerminalControl |
126127

127128
See the [Controls Reference](docs/CONTROLS.md) for detailed documentation on each control.
@@ -275,6 +276,7 @@ dotnet run --project Examples/DemoApp
275276
| **[Registry](docs/REGISTRY.md)** | Persistent hierarchical key-value storage |
276277
| **[Plugins](docs/PLUGINS.md)** | Plugin architecture and development |
277278
| **[Gradients & Alpha](docs/GRADIENTS.md)** | Gradient text, window backgrounds, transparent control compositing |
279+
| **[Video Playback](docs/VIDEO_PLAYBACK.md)** | Terminal video player — FFmpeg decode, half-block/ASCII/braille modes |
278280
| **[Compositor Effects](docs/COMPOSITOR_EFFECTS.md)** | Buffer manipulation and visual effects |
279281
| **[DOM Layout System](docs/DOM_LAYOUT_SYSTEM.md)** | Layout engine internals |
280282
| **[Rendering Pipeline](docs/RENDERING_PIPELINE.md)** | Rendering architecture details |

SharpConsoleUI/Builders/Controls.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,19 @@ public static CanvasControlBuilder Canvas(int? width = null, int? height = null)
314314
return builder;
315315
}
316316

317+
/// <summary>
318+
/// Creates a VideoControl builder for terminal video playback.
319+
/// </summary>
320+
/// <returns>A new video control builder.</returns>
321+
public static VideoControlBuilder Video() => new VideoControlBuilder();
322+
323+
/// <summary>
324+
/// Creates a VideoControl builder with a file path pre-set.
325+
/// </summary>
326+
/// <param name="filePath">Path to the video file.</param>
327+
/// <returns>A new video control builder.</returns>
328+
public static VideoControlBuilder Video(string filePath) => new VideoControlBuilder().WithFile(filePath);
329+
317330
/// <summary>
318331
/// Creates a new navigation view builder.
319332
/// </summary>
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// -----------------------------------------------------------------------
2+
// ConsoleEx - A simple console window system for .NET Core
3+
//
4+
// Author: Nikolaos Protopapas
5+
// Email: nikolaos.protopapas@gmail.com
6+
// License: MIT
7+
// -----------------------------------------------------------------------
8+
9+
using SharpConsoleUI.Controls;
10+
using SharpConsoleUI.DataBinding;
11+
using SharpConsoleUI.Layout;
12+
using SharpConsoleUI.Video;
13+
14+
namespace SharpConsoleUI.Builders;
15+
16+
/// <summary>
17+
/// Fluent builder for <see cref="VideoControl"/>.
18+
/// </summary>
19+
public sealed class VideoControlBuilder : IControlBuilder<VideoControl>
20+
{
21+
private readonly VideoControl _control = new();
22+
23+
/// <summary>Sets the video file path.</summary>
24+
public VideoControlBuilder WithFile(string filePath)
25+
{
26+
_control.FilePath = filePath;
27+
return this;
28+
}
29+
30+
/// <summary>Sets the render mode.</summary>
31+
public VideoControlBuilder WithRenderMode(VideoRenderMode mode)
32+
{
33+
_control.RenderMode = mode;
34+
return this;
35+
}
36+
37+
/// <summary>Sets the target frames per second.</summary>
38+
public VideoControlBuilder WithTargetFps(int fps)
39+
{
40+
_control.TargetFps = fps;
41+
return this;
42+
}
43+
44+
/// <summary>Enables looping playback.</summary>
45+
public VideoControlBuilder WithLooping(bool loop = true)
46+
{
47+
_control.Looping = loop;
48+
return this;
49+
}
50+
51+
/// <summary>Enables the bottom overlay status bar (auto-show on interaction, auto-hide after 3s).</summary>
52+
public VideoControlBuilder WithOverlay(bool enabled = true)
53+
{
54+
_control.OverlayEnabled = enabled;
55+
return this;
56+
}
57+
58+
/// <summary>Sets horizontal alignment.</summary>
59+
public VideoControlBuilder WithAlignment(HorizontalAlignment alignment)
60+
{
61+
_control.HorizontalAlignment = alignment;
62+
return this;
63+
}
64+
65+
/// <summary>Sets vertical alignment.</summary>
66+
public VideoControlBuilder WithVerticalAlignment(VerticalAlignment alignment)
67+
{
68+
_control.VerticalAlignment = alignment;
69+
return this;
70+
}
71+
72+
/// <summary>Stretch horizontally to fill.</summary>
73+
public VideoControlBuilder Stretch()
74+
{
75+
_control.HorizontalAlignment = HorizontalAlignment.Stretch;
76+
return this;
77+
}
78+
79+
/// <summary>Fill vertically and stretch horizontally.</summary>
80+
public VideoControlBuilder Fill()
81+
{
82+
_control.VerticalAlignment = VerticalAlignment.Fill;
83+
_control.HorizontalAlignment = HorizontalAlignment.Stretch;
84+
return this;
85+
}
86+
87+
/// <summary>Sets control margin.</summary>
88+
public VideoControlBuilder WithMargin(int left, int top, int right, int bottom)
89+
{
90+
_control.Margin = new Margin(left, top, right, bottom);
91+
return this;
92+
}
93+
94+
/// <summary>Sets the control name.</summary>
95+
public VideoControlBuilder WithName(string name)
96+
{
97+
_control.Name = name;
98+
return this;
99+
}
100+
101+
/// <summary>Subscribes to playback state changes.</summary>
102+
public VideoControlBuilder OnPlaybackStateChanged(EventHandler<VideoPlaybackState> handler)
103+
{
104+
_control.PlaybackStateChanged += handler;
105+
return this;
106+
}
107+
108+
/// <summary>Subscribes to playback ended.</summary>
109+
public VideoControlBuilder OnPlaybackEnded(EventHandler handler)
110+
{
111+
_control.PlaybackEnded += handler;
112+
return this;
113+
}
114+
115+
/// <summary>Builds the VideoControl.</summary>
116+
public VideoControl Build() => _control;
117+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
using SharpConsoleUI.Video;
2+
3+
namespace SharpConsoleUI.Configuration
4+
{
5+
/// <summary>
6+
/// Default constants for VideoControl. Separated from ControlDefaults
7+
/// to avoid a dependency from Configuration on the Video namespace.
8+
/// </summary>
9+
public static class VideoDefaults
10+
{
11+
/// <summary>Default render mode for VideoControl.</summary>
12+
public const VideoRenderMode DefaultRenderMode = VideoRenderMode.HalfBlock;
13+
14+
/// <summary>Target frames per second for video playback.</summary>
15+
public const int DefaultTargetFps = 30;
16+
17+
/// <summary>Maximum frames to skip when playback falls behind.</summary>
18+
public const int MaxFrameSkip = 5;
19+
20+
/// <summary>
21+
/// Number of frames behind before frame-skipping activates.
22+
/// </summary>
23+
public const int FrameSkipThreshold = 2;
24+
25+
/// <summary>Seek jump in seconds for Left/Right arrow keys.</summary>
26+
public const double SeekStepSeconds = 5.0;
27+
28+
/// <summary>FFmpeg process start timeout in milliseconds.</summary>
29+
public const int FfmpegStartTimeoutMs = 5000;
30+
31+
/// <summary>FFmpeg read timeout per frame in milliseconds.</summary>
32+
public const int FfmpegReadTimeoutMs = 2000;
33+
34+
/// <summary>Delay in milliseconds between pause-state polls.</summary>
35+
public const int PausePollDelayMs = 50;
36+
37+
/// <summary>Minimum sleep threshold in milliseconds before Task.Delay is worthwhile.</summary>
38+
public const double MinSleepThresholdMs = 1.0;
39+
40+
/// <summary>Half-block pixels per cell (vertical).</summary>
41+
public const int HalfBlockPixelsPerCell = 2;
42+
43+
/// <summary>ASCII brightness-to-density character ramp (darkest to brightest).</summary>
44+
public const string AsciiDensityRamp = " .:-=+*#%@";
45+
46+
/// <summary>Braille Unicode base codepoint (U+2800).</summary>
47+
public const int BrailleBaseCodepoint = 0x2800;
48+
49+
/// <summary>Braille cell width in source pixels.</summary>
50+
public const int BrailleCellPixelWidth = 2;
51+
52+
/// <summary>Braille cell height in source pixels.</summary>
53+
public const int BrailleCellPixelHeight = 4;
54+
55+
/// <summary>Braille brightness threshold (0-255) for dot activation.</summary>
56+
public const int BrailleBrightnessThreshold = 64;
57+
58+
/// <summary>Default fallback cell columns when control hasn't been laid out.</summary>
59+
public const int FallbackCellCols = 80;
60+
61+
/// <summary>Default fallback cell rows when control hasn't been laid out.</summary>
62+
public const int FallbackCellRows = 24;
63+
64+
/// <summary>Minimum FPS clamp.</summary>
65+
public const int MinFps = 1;
66+
67+
/// <summary>Maximum FPS clamp.</summary>
68+
public const int MaxFps = 120;
69+
70+
/// <summary>Overlay auto-hide timeout in milliseconds after last interaction.</summary>
71+
public const int OverlayAutoHideMs = 3000;
72+
73+
/// <summary>Overlay height in cell rows.</summary>
74+
public const int OverlayHeight = 1;
75+
76+
/// <summary>FFmpeg not found error message displayed inside the control.</summary>
77+
public const string FfmpegNotFoundMessage =
78+
"FFmpeg not found. Install it to play videos:\n" +
79+
" Linux: sudo apt install ffmpeg\n" +
80+
" macOS: brew install ffmpeg\n" +
81+
" Windows: winget install ffmpeg";
82+
}
83+
}

0 commit comments

Comments
 (0)