Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 36 additions & 31 deletions .github/workflows/autorelease.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,41 +3,46 @@ name: "pre-release"
on:
push:
branches:
- "master"
- "main"

jobs:
pre-release:
name: "Pre Release"
runs-on: "ubuntu-latest"

steps:
- uses: actions/checkout@v1

- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: '10.0.x'

- name: Install zip
uses: montudor/action-zip@v1

- name: Build with dotnet
working-directory: ./
run: |
dotnet publish -c Release -r linux-x64 --self-contained true -p:PublishReadyToRun=true -p:PublishSingleFile=true -o linux
dotnet publish -c Release -r win-x64 --self-contained true -p:PublishReadyToRun=true -p:PublishSingleFile=true -o windows
dotnet publish -c Release -r osx-x64 --self-contained true -p:PublishReadyToRun=true -p:PublishSingleFile=true -o macos

- name: Zip output
run: zip -qq -r Release.zip Release
working-directory: ./

- name: Publish GitHub Release
uses: "marvinpinto/action-automatic-releases@latest"
with:
repo_token: "${{ secrets.GITHUB_TOKEN }}"
automatic_release_tag: "latest"
prerelease: true
title: "Development Build"
files: |
./bin/Release.zip
- uses: actions/checkout@v4

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: "10.0.x"

- name: Install zip
uses: montudor/action-zip@v1

- name: Build with dotnet
working-directory: ./
run: |
dotnet publish logo.csproj -c Release -r linux-x64 --self-contained true -p:PublishReadyToRun=true -p:PublishSingleFile=true -o artifacts/release/linux-x64
dotnet publish logo.csproj -c Release -r win-x64 --self-contained true -p:PublishReadyToRun=true -p:PublishSingleFile=true -o artifacts/release/win-x64
dotnet publish logo.csproj -c Release -r osx-x64 --self-contained true -p:PublishReadyToRun=true -p:PublishSingleFile=true -o artifacts/release/osx-x64

- name: Zip outputs
run: |
zip -qq -r logo-linux-x64.zip artifacts/release/linux-x64
zip -qq -r logo-win-x64.zip artifacts/release/win-x64
zip -qq -r logo-osx-x64.zip artifacts/release/osx-x64
working-directory: ./

- name: Publish GitHub Release
uses: "marvinpinto/action-automatic-releases@latest"
with:
repo_token: "${{ secrets.GITHUB_TOKEN }}"
automatic_release_tag: "latest"
prerelease: true
title: "Development Build"
files: |
./logo-linux-x64.zip
./logo-win-x64.zip
./logo-osx-x64.zip
46 changes: 32 additions & 14 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
@@ -1,26 +1,44 @@
name: .NET Build

# Trigger the workflow on push or pull request
on: [push]
on:
push:
branches:
- main
- encoder
pull_request:
branches:
- main

jobs:
build:

strategy:
matrix:
os: [macos-latest, windows-latest, ubuntu-latest]
fail-fast: false

runs-on: ${{ matrix.os }}

steps:
- uses: actions/checkout@v1

- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: '10.0.x'

- name: Build with dotnet
working-directory: ./
run: dotnet build --configuration Release
- uses: actions/checkout@v4

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: "10.0.x"

- name: Restore
working-directory: ./
run: |
dotnet restore tools/Hnm0Encoder/Hnm0Encoder.csproj
dotnet restore HnmPlayer/HnmPlayer.csproj
dotnet restore tests/HnmEndToEndTests/HnmEndToEndTests.csproj

- name: Build encoder and player
working-directory: ./
run: |
dotnet build tools/Hnm0Encoder/Hnm0Encoder.csproj --configuration Release --no-restore
dotnet build HnmPlayer/HnmPlayer.csproj --configuration Release --no-restore

- name: Test
working-directory: ./
run: dotnet test tests/HnmEndToEndTests/HnmEndToEndTests.csproj --configuration Release
5 changes: 5 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<Project>
<PropertyGroup>
<NoWarn>$(NoWarn);NU1902;NU1903</NoWarn>
</PropertyGroup>
</Project>
6 changes: 5 additions & 1 deletion HnmPlayer/App.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Themes.Fluent;
using System.Runtime.Versioning;
Comment on lines 1 to +4

namespace logo;

Expand All @@ -11,11 +12,14 @@ public override void Initialize()
Styles.Add(new FluentTheme());
}

[SupportedOSPlatform("windows")]
public override void OnFrameworkInitializationCompleted()
{
Comment on lines +15 to 17
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow();
desktop.MainWindow = PlayerLaunchContext.Current.Headless
? new HeadlessCaptureWindow()
: new MainWindow();
}

base.OnFrameworkInitializationCompleted();
Expand Down
34 changes: 34 additions & 0 deletions HnmPlayer/HeadlessCaptureWindow.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media.Imaging;
using System.Diagnostics;

namespace logo;

public sealed class HeadlessCaptureWindow : Window
{
public HeadlessCaptureWindow()
{
Opened += OnOpened;
}

private async void OnOpened(object? sender, EventArgs e)
{
Debug.Assert(PlayerLaunchContext.Current.Headless, "Headless capture window should only be used in headless mode.");
if (string.IsNullOrWhiteSpace(PlayerLaunchContext.Current.InputPath))
{
throw new InvalidOperationException("Headless mode requires an input path.");
}

if (string.IsNullOrWhiteSpace(PlayerLaunchContext.Current.ScreenshotDirectory))
{
throw new InvalidOperationException("Headless mode requires a screenshot directory.");
}

string inputPath = PlayerLaunchContext.Current.InputPath;
string screenshotDirectory = PlayerLaunchContext.Current.ScreenshotDirectory;

await HeadlessPlayerRunner.RunAsync(inputPath, screenshotDirectory);
Close();
}
}
60 changes: 60 additions & 0 deletions HnmPlayer/HeadlessPlayerRunner.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using Avalonia;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using System.Diagnostics;

namespace logo;

internal static class HeadlessPlayerRunner
{
public static async Task RunAsync(string inputPath, string screenshotDirectory)
{
Debug.Assert(!string.IsNullOrWhiteSpace(inputPath), "Input path must not be empty.");
Debug.Assert(!string.IsNullOrWhiteSpace(screenshotDirectory), "Screenshot directory must not be empty.");

Directory.CreateDirectory(screenshotDirectory);

var engine = new HnmPlaybackEngine();
await engine.LoadAsync(inputPath);

using WriteableBitmap bitmap = new(
new PixelSize(HnmPlaybackEngine.FrameWidth, HnmPlaybackEngine.FrameHeight),
new Vector(96, 96),
Avalonia.Platform.PixelFormat.Bgra8888,
AlphaFormat.Opaque);

int screenshotIndex = 0;
SaveScreenshot(engine, bitmap, Path.Combine(screenshotDirectory, FormatScreenshotName(screenshotIndex++)));

while (true)
{
HnmStepResult stepResult = engine.Step();
if (!stepResult.Advanced)
{
break;
}

if (stepResult.VisualChanged)
{
SaveScreenshot(engine, bitmap, Path.Combine(screenshotDirectory, FormatScreenshotName(screenshotIndex++)));
}
}
}

private static void SaveScreenshot(HnmPlaybackEngine engine, WriteableBitmap bitmap, string path)
{
engine.RenderCurrentFrame(bitmap);
WriteableBitmapToPng(bitmap, path);
}

private static void WriteableBitmapToPng(WriteableBitmap bitmap, string path)
{
using FileStream stream = File.Create(path);
bitmap.Save(stream);

Check warning on line 53 in HnmPlayer/HeadlessPlayerRunner.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

'Bitmap.Save(Stream, int?)' is obsolete: 'Use the overload accepting BitmapEncoderOptions instead.'

Check warning on line 53 in HnmPlayer/HeadlessPlayerRunner.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

'Bitmap.Save(Stream, int?)' is obsolete: 'Use the overload accepting BitmapEncoderOptions instead.'

Check warning on line 53 in HnmPlayer/HeadlessPlayerRunner.cs

View workflow job for this annotation

GitHub Actions / build (macos-latest)

'Bitmap.Save(Stream, int?)' is obsolete: 'Use the overload accepting BitmapEncoderOptions instead.'

Check warning on line 53 in HnmPlayer/HeadlessPlayerRunner.cs

View workflow job for this annotation

GitHub Actions / build (macos-latest)

'Bitmap.Save(Stream, int?)' is obsolete: 'Use the overload accepting BitmapEncoderOptions instead.'

Check warning on line 53 in HnmPlayer/HeadlessPlayerRunner.cs

View workflow job for this annotation

GitHub Actions / build (windows-latest)

'Bitmap.Save(Stream, int?)' is obsolete: 'Use the overload accepting BitmapEncoderOptions instead.'

Check warning on line 53 in HnmPlayer/HeadlessPlayerRunner.cs

View workflow job for this annotation

GitHub Actions / build (windows-latest)

'Bitmap.Save(Stream, int?)' is obsolete: 'Use the overload accepting BitmapEncoderOptions instead.'

Check warning on line 53 in HnmPlayer/HeadlessPlayerRunner.cs

View workflow job for this annotation

GitHub Actions / build (macos-latest)

'Bitmap.Save(Stream, int?)' is obsolete: 'Use the overload accepting BitmapEncoderOptions instead.'

Check warning on line 53 in HnmPlayer/HeadlessPlayerRunner.cs

View workflow job for this annotation

GitHub Actions / build (macos-latest)

'Bitmap.Save(Stream, int?)' is obsolete: 'Use the overload accepting BitmapEncoderOptions instead.'

Check warning on line 53 in HnmPlayer/HeadlessPlayerRunner.cs

View workflow job for this annotation

GitHub Actions / build (windows-latest)

'Bitmap.Save(Stream, int?)' is obsolete: 'Use the overload accepting BitmapEncoderOptions instead.'

Check warning on line 53 in HnmPlayer/HeadlessPlayerRunner.cs

View workflow job for this annotation

GitHub Actions / build (windows-latest)

'Bitmap.Save(Stream, int?)' is obsolete: 'Use the overload accepting BitmapEncoderOptions instead.'

Check warning on line 53 in HnmPlayer/HeadlessPlayerRunner.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

'Bitmap.Save(Stream, int?)' is obsolete: 'Use the overload accepting BitmapEncoderOptions instead.'

Check warning on line 53 in HnmPlayer/HeadlessPlayerRunner.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

'Bitmap.Save(Stream, int?)' is obsolete: 'Use the overload accepting BitmapEncoderOptions instead.'
}

private static string FormatScreenshotName(int index)
{
return $"frame{index:0000}.png";
}
}
10 changes: 7 additions & 3 deletions HnmPlayer/HnmPlayer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,15 @@
<Compile Include="MainWindow.cs" />
<Compile Include="CirclesPaletteData.cs" />
<Compile Include="HnmPlaybackEngine.cs" />
<Compile Include="PlayerLaunchContext.cs" />
<Compile Include="HeadlessCaptureWindow.cs" />
<Compile Include="HeadlessPlayerRunner.cs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Avalonia" Version="12.0.3" />
<PackageReference Include="Avalonia.Desktop" Version="12.0.3" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="12.0.3" />
<PackageReference Include="Avalonia" Version="12.1.1" />
<PackageReference Include="Avalonia.Desktop" Version="12.1.1" />
<PackageReference Include="Avalonia.Headless" Version="12.1.1" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="12.1.1" />
</ItemGroup>
</Project>
77 changes: 77 additions & 0 deletions HnmPlayer/PlayerLaunchContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
namespace logo;

internal static class PlayerLaunchContext
{
public static PlayerLaunchOptions Current { get; set; } = PlayerLaunchOptions.UiDefault;
}

public sealed record PlayerLaunchOptions(bool Headless, string? InputPath, string? ScreenshotDirectory)
{
public static PlayerLaunchOptions UiDefault { get; } = new(false, null, null);
}

internal static class PlayerCommandLineParser
{
public static PlayerLaunchOptions Parse(string[] args)
{
bool headless = false;
string? inputPath = null;
string? screenshotDirectory = null;

for (int i = 0; i < args.Length; i++)
{
string arg = args[i];
switch (arg)
{
case "--headless":
headless = true;
break;
case "--input":
inputPath = ReadValue(args, ref i, arg);
break;
case "--screenshot-dir":
screenshotDirectory = ReadValue(args, ref i, arg);
break;
default:
throw new InvalidOperationException($"Unknown argument '{arg}'.");
}
}

if (!headless)
{
return PlayerLaunchOptions.UiDefault;
}

if (string.IsNullOrWhiteSpace(inputPath))
{
throw new InvalidOperationException("Missing required argument --input for headless mode.");
}

if (string.IsNullOrWhiteSpace(screenshotDirectory))
{
throw new InvalidOperationException("Missing required argument --screenshot-dir for headless mode.");
}

return new PlayerLaunchOptions(
true,
Path.GetFullPath(inputPath),
Path.GetFullPath(screenshotDirectory));
}

private static string ReadValue(string[] args, ref int index, string argName)
{
if (index + 1 >= args.Length)
{
throw new InvalidOperationException($"Missing value after {argName}.");
}

index++;
string value = args[index];
if (string.IsNullOrWhiteSpace(value))
{
throw new InvalidOperationException($"Argument {argName} requires a non-empty value.");
}

return value;
}
}
22 changes: 18 additions & 4 deletions HnmPlayer/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Avalonia;
using Avalonia.Headless;

namespace logo;

Expand All @@ -7,10 +8,23 @@ public static class Program
[STAThread]
public static void Main(string[] args)
{
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
PlayerLaunchOptions launchOptions = PlayerCommandLineParser.Parse(args);
PlayerLaunchContext.Current = launchOptions;
BuildAvaloniaApp(launchOptions).StartWithClassicDesktopLifetime(args);
}

public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect();
public static AppBuilder BuildAvaloniaApp(PlayerLaunchOptions launchOptions)
{
AppBuilder builder = AppBuilder.Configure<App>();
if (launchOptions.Headless)
{
builder = builder.UseHeadless(new Avalonia.Headless.AvaloniaHeadlessPlatformOptions());
}
else
{
builder = builder.UsePlatformDetect();
}

return builder;
}
}
Loading
Loading