Minimal, zero-dependency player for ConsoleImage JSON documents. Play pre-rendered ASCII art animations in any terminal. Perfect for embedding in your own applications - just pass a JSON string or file path.
$ dotnet add package mostlylucid.consoleimage.playerA minimal, zero-dependency player for ConsoleImage documents. Play pre-rendered ASCII art and animations in any .NET terminal app - no ImageSharp, no FFmpeg, no external packages.
| Feature | Details |
|---|---|
| Dependencies | None (only built-in System.Text.Json) |
| Package size | ~50 KB |
| AOT compatible | Yes - source-generated JSON, no reflection |
| Formats | .cidz (compressed), .json, .ndjson (streaming) |
| Frame parse time | ~100–300 µs per frame |
dotnet add package mostlylucid.consoleimage.player
.cidz AnimationThis walkthrough creates a self-contained CLI app that plays an animated logo on startup. By the end you will have a working project with an embedded .cidz file.
You need the consoleimage CLI to render images/GIFs into .cidz documents. Install it as a .NET global tool:
dotnet tool install -g mostlylucid.consoleimage
Or build from source: dotnet build in the ConsoleImage directory.
.cidz fileRender any image or GIF to a compressed document:
# Still image → single-frame document
consoleimage logo.png -w 60 --braille -o logo.cidz
# Animated GIF → multi-frame document (animation preserved)
consoleimage animation.gif -w 60 -o animation.cidz
# Color blocks mode (higher fidelity, uses Unicode half-blocks)
consoleimage photo.jpg -w 80 --blocks -o photo.cidz
# Classic ASCII characters
consoleimage banner.png -w 100 --ascii -o banner.cidz
# Video clip (first 5 seconds)
consoleimage intro.mp4 -w 60 -t 5 -o intro.cidz
What is
.cidz? A GZip-compressed JSON format with delta encoding. A 2 MB uncompressed JSON shrinks to ~300 KB. The Player handles decompression transparently.
mkdir MyApp && cd MyApp
dotnet new console
dotnet add package mostlylucid.consoleimage.player
.cidz file to your projectCopy animation.cidz into the project root, then add to MyApp.csproj:
<ItemGroup>
<EmbeddedResource Include="animation.cidz" />
</ItemGroup>
Replace Program.cs with:
using System.Reflection;
using ConsoleImage.Player;
// Load the embedded .cidz resource
var assembly = Assembly.GetExecutingAssembly();
// Resource name = default namespace + filename (dots replace folder separators)
await using var stream = assembly.GetManifestResourceStream("MyApp.animation.cidz");
if (stream is null)
{
Console.Error.WriteLine("Resource not found. Check the name matches your namespace.");
return 1;
}
// Read the compressed bytes
using var ms = new MemoryStream();
await stream.CopyToAsync(ms);
var doc = PlayerDocument.FromCompressedBytes(ms.ToArray());
// Play once, then continue
using var player = new ConsolePlayer(doc, loopCount: 1);
await player.PlayAsync();
Console.WriteLine();
Console.WriteLine("Welcome to MyApp!");
return 0;
dotnet run
The animation plays through once, then your app continues normally. Press Ctrl+C at any time to cancel.
The Player is fully AOT-compatible:
dotnet publish -c Release -r win-x64 /p:PublishAot=true
using ConsoleImage.Player;
// Auto-detects format: .cidz, .json, or .ndjson
using var player = await ConsolePlayer.FromFileAsync("animation.cidz");
await player.PlayAsync();
using ConsoleImage.Player;
string json = File.ReadAllText("animation.json");
using var player = ConsolePlayer.FromJson(json);
await player.PlayAsync();
using var player = await ConsolePlayer.FromFileAsync("photo.cidz");
player.Display(); // Writes the first frame to stdout
var doc = await PlayerDocument.LoadAsync("animation.cidz");
Console.WriteLine($"Frames: {doc.FrameCount}");
Console.WriteLine($"Duration: {doc.TotalDurationMs}ms");
Console.WriteLine($"Mode: {doc.RenderMode}"); // ASCII, Braille, ColorBlocks, Matrix
foreach (var frame in doc.Frames)
{
Console.Write(frame.Content); // ANSI-escaped string
await Task.Delay(frame.DelayMs);
}
The high-level player. Handles cursor management, synchronized output, and animation timing.
// Constructor
new ConsolePlayer(
PlayerDocument document,
float? speedMultiplier = null, // null = use document default
int? loopCount = null, // null = use document default, 0 = infinite
string? subtitlePath = null // optional SRT/VTT file
)
// Static factories (convenience)
await ConsolePlayer.FromFileAsync("file.cidz", speedMultiplier: 1.5f, loopCount: 3);
ConsolePlayer.FromJson(jsonString, loopCount: 1);
// Playback controls (can be set before or during playback)
player.SpeedMultiplier = 2.0f; // Runtime speed change (> 1 = faster)
player.MaxDurationMs = 5000; // Stop after 5s of content time
player.StartFrame = 10; // Start at frame 10
player.EndFrame = 50; // Stop before frame 50
player.FrameStep = 2; // Play every other frame (downsampling)
// Playback
await player.PlayAsync(cancellationToken); // Animated playback
player.Display(); // First frame only
player.Display(showAllFrames: true); // Dump all frames (debug)
// Info
string info = player.GetInfo(); // Formatted metadata
PlayerDocument doc = player.Document; // Access underlying document
// Events
player.OnFrameChanged += (current, total) => { };
player.OnLoopComplete += (loopNumber) => { };
// Cleanup
player.Dispose(); // or use `using`
Loop count values:
0 = loop forever (until cancelled)1 = play onceN = play N timesPlayback control properties:
| Property | Type | Default | Description |
|---|---|---|---|
SpeedMultiplier | float | from doc | Playback speed (2.0 = 2x, 0.5 = half). Can change during playback. |
MaxDurationMs | int? | null | Stop after N ms of content time. Null = no limit. |
StartFrame | int? | null | First frame to play (0-based). Null = beginning. |
EndFrame | int? | null | Frame to stop before (exclusive). Null = end. |
FrameStep | int | 1 | Play every Nth frame. 2 = skip alternate frames. |
Represents a loaded document with all frames. Load from any supported format:
// From file (auto-detects format)
var doc = await PlayerDocument.LoadAsync("file.cidz", cancellationToken);
// From JSON string
var doc = PlayerDocument.FromJson(jsonString);
// From compressed byte array (e.g. embedded resource)
var doc = PlayerDocument.FromCompressedBytes(byteArray);
// From compressed stream
var doc = await PlayerDocument.FromCompressedStreamAsync(stream, cancellationToken);
// Properties
doc.FrameCount // int - number of frames
doc.IsAnimated // bool - true if more than 1 frame
doc.TotalDurationMs // int - sum of all frame delays
doc.RenderMode // string - "ASCII", "ColorBlocks", "Braille", "Matrix"
doc.Version // string - format version
doc.Created // DateTime - when the document was created
doc.SourceFile // string? - original source file name
doc.Settings // PlayerSettings - render settings
doc.Frames // List<PlayerFrame> - all frames
A single frame of content:
frame.Content // string - ANSI-escaped terminal content
frame.DelayMs // int - milliseconds before next frame
frame.Width // int - width in characters
frame.Height // int - height in lines
Render settings stored in the document:
settings.MaxWidth // int (default 120)
settings.MaxHeight // int (default 60)
settings.CharacterAspectRatio // float (default 0.5)
settings.UseColor // bool (default true)
settings.AnimationSpeedMultiplier // float (default 1.0)
settings.LoopCount // int (default 0 = infinite)
Static utilities (called automatically, but available if needed):
ConsoleHelper.EnableAnsiSupport(); // Enable ANSI on Windows
ConsoleHelper.IsAnsiSupported; // Check ANSI support
ConsoleHelper.DetectCellAspectRatio(); // Auto-detect terminal font ratio
The Player reads three formats. All are created by the consoleimage CLI or the ConsoleImage.Core library.
| Format | Extension | Best for | Size |
|---|---|---|---|
| Compressed | .cidz | Everything (default) | Smallest (~7:1 ratio) |
| Standard JSON | .json | Debugging, interop | Large |
| Streaming NDJSON | .ndjson | Long videos | Large (line-by-line) |
# Compressed (recommended) - auto-selected for .cidz extension
consoleimage input.gif -w 80 -o output.cidz
# Uncompressed JSON - use raw: prefix to force uncompressed
consoleimage input.gif -w 80 -o raw:output.json
# Streaming NDJSON - for very long videos, auto-finalizes on Ctrl+C
consoleimage input.mp4 -w 80 -o output.ndjson
# With de-jitter (reduces color flickering in animations)
consoleimage input.gif -w 80 -o output.cidz --dejitter
PlayerDocument.LoadAsync() auto-detects format:
0x1F 0x8B) → decompresses and parsesYou never need to specify the format - just pass the file path.
For self-contained apps, embed the .cidz as a resource.
<ItemGroup>
<EmbeddedResource Include="assets/splash.cidz" />
</ItemGroup>
using System.Reflection;
using ConsoleImage.Player;
// Resource name format: {DefaultNamespace}.{folder}.{filename}
// Dots replace folder separators. Example:
// Project namespace: MyApp
// File path: assets/splash.cidz
// Resource name: MyApp.assets.splash.cidz
var asm = Assembly.GetExecutingAssembly();
await using var stream = asm.GetManifestResourceStream("MyApp.assets.splash.cidz");
if (stream is null)
{
// Debug: list all resource names to find the right one
foreach (var name in asm.GetManifestResourceNames())
Console.Error.WriteLine($" Resource: {name}");
return;
}
using var ms = new MemoryStream();
await stream.CopyToAsync(ms);
var doc = PlayerDocument.FromCompressedBytes(ms.ToArray());
using var player = new ConsolePlayer(doc, loopCount: 1);
await player.PlayAsync();
Common mistake: The resource name uses dots, not slashes.
assets/splash.cidzbecomesMyApp.assets.splash.cidz. If loading fails, enumerateGetManifestResourceNames()to find the correct name.
All async methods accept CancellationToken. Use Ctrl+C or cancel programmatically:
using var cts = new CancellationTokenSource();
// Cancel after 10 seconds
cts.CancelAfter(TimeSpan.FromSeconds(10));
// Or cancel on keypress
_ = Task.Run(() => { Console.ReadKey(true); cts.Cancel(); });
using var player = await ConsolePlayer.FromFileAsync("animation.cidz");
await player.PlayAsync(cts.Token);
// Continues here after cancellation or completion
The player always restores cursor visibility and resets colors in its finally block, even when cancelled.
using var player = await ConsolePlayer.FromFileAsync("animation.cidz");
player.OnFrameChanged += (current, total) =>
{
var percent = (current * 100) / total;
Console.Title = $"Playing... {percent}%";
};
player.OnLoopComplete += loopNumber =>
{
Console.Title = $"Loop {loopNumber} complete";
};
await player.PlayAsync();
try
{
var doc = await PlayerDocument.LoadAsync("file.cidz");
using var player = new ConsolePlayer(doc);
await player.PlayAsync();
}
catch (FileNotFoundException)
{
Console.Error.WriteLine("Document file not found.");
}
catch (System.Text.Json.JsonException ex)
{
Console.Error.WriteLine($"Invalid document format: {ex.Message}");
}
catch (InvalidOperationException ex)
{
Console.Error.WriteLine($"Failed to parse document: {ex.Message}");
}
| Document Size | Frames | Load Time | Per Frame |
|---|---|---|---|
| 181 KB | 9 | 1.1 ms | ~120 µs |
| 724 KB | 31 | 3.1 ms | ~100 µs |
| 2 MB | 59 | 9.1 ms | ~155 µs |
LoadAsync and PlayAsync are fully async with CancellationToken supportFromJson is synchronous - use for small documents or when you already have the stringConsole.Write - do not write to the console from another thread during PlayAsyncCharacters appear garbled (boxes, question marks)
Your terminal doesn't support Unicode or ANSI escape codes. On Windows, use Windows Terminal (not the legacy cmd.exe window). The Player calls ConsoleHelper.EnableAnsiSupport() automatically, but legacy consoles may not support 24-bit color.
Animation flickers Your terminal may not support DECSET 2026 (synchronized output). Windows Terminal and most modern Linux terminals support it. Legacy terminals will still work but may flicker between frames.
Embedded resource returns null Resource names use dots as separators. Print all names to find yours:
foreach (var name in Assembly.GetExecutingAssembly().GetManifestResourceNames())
Console.WriteLine(name);
File loads but shows nothing
The document may have been rendered for a light terminal while you're on a dark one (or vice versa). Re-export with appropriate settings. Most documents use Invert = true (dark terminal default).
.json file is huge
Use .cidz instead - it's the same content with ~7:1 compression via delta encoding and GZip. The CLI defaults to .cidz when you specify a .json extension; use raw:output.json to force uncompressed.
A production-ready example showing graceful fallback, cancellation, and AOT compatibility.
MyApp.csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<PublishAot>true</PublishAot>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="mostlylucid.consoleimage.player" Version="*" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="splash.cidz" />
</ItemGroup>
</Project>
Program.cs:
using System.Reflection;
using ConsoleImage.Player;
// Allow Ctrl+C to cancel the splash gracefully
using var cts = new CancellationTokenSource();
Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); };
await PlaySplashAsync(cts.Token);
// App continues normally after splash
Console.WriteLine("App ready.");
static async Task PlaySplashAsync(CancellationToken ct)
{
try
{
var asm = Assembly.GetExecutingAssembly();
await using var stream = asm.GetManifestResourceStream("MyApp.splash.cidz");
if (stream is null) return; // No splash - silently continue
using var ms = new MemoryStream();
await stream.CopyToAsync(ms, ct);
var doc = PlayerDocument.FromCompressedBytes(ms.ToArray());
using var player = new ConsolePlayer(doc, loopCount: 1);
await player.PlayAsync(ct);
}
catch (OperationCanceledException)
{
// User pressed Ctrl+C during splash - continue to app
}
catch (Exception ex)
{
// Splash failed - log and continue (never crash on splash)
Console.Error.WriteLine($"Splash error: {ex.Message}");
}
}
Unlicense - Public Domain