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
27 changes: 23 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ name: CI

on:
push:
branches: [ main, develop ]
branches: [ master ]
pull_request:
branches: [ main, develop ]
branches: [ master ]

jobs:
build-and-test:
name: Build & Test
build-windows:
name: Build & Test (Windows)
runs-on: windows-latest

steps:
Expand All @@ -28,3 +28,22 @@ jobs:

- name: Test
run: dotnet test --no-build --configuration Release --verbosity normal

build-linux:
name: Build (Linux)
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

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

- name: Restore
run: dotnet restore

- name: Build
run: dotnet build --no-restore --configuration Release
23 changes: 21 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ jobs:
run: |
dotnet publish ${{ env.PROJECT }} \
--configuration Release \
--runtime osx-x64 \
--runtime osx-arm64 \
--self-contained true \
--output publish/macos \
-p:Version=${{ needs.metadata.outputs.version }} \
Expand All @@ -149,6 +149,17 @@ jobs:
- name: Copy OBS scripts
run: cp -r obs-scripts publish/macos/obs-scripts

- name: Add macOS launcher script
run: |
cp clipstudio-mac.sh publish/macos/ClipStudio.sh
chmod +x publish/macos/ClipStudio.sh
chmod +x publish/macos/ClipStudio.UI

- name: Ad-hoc code sign
run: |
codesign --deep --force --sign - publish/macos/ClipStudio.UI
codesign --deep --force --sign - publish/macos/ClipStudio.sh

- name: Bundle FFmpeg (macOS)
run: |
brew install ffmpeg
Expand All @@ -163,7 +174,7 @@ jobs:
--packId ClipStudio \
--packVersion ${{ needs.metadata.outputs.version }} \
--packDir publish/macos \
--mainExe ClipStudio.UI \
--mainExe ClipStudio.sh \
--outputDir releases/macos \
--channel ${{ needs.metadata.outputs.channel }} \
--packTitle "ClipStudio" \
Expand Down Expand Up @@ -199,6 +210,9 @@ jobs:
- name: Restore
run: dotnet restore

- name: Test
run: dotnet test --configuration Release --no-restore --verbosity minimal

- name: Publish
run: |
dotnet publish ${{ env.PROJECT }} \
Expand All @@ -213,6 +227,11 @@ jobs:
- name: Copy OBS scripts
run: cp -r obs-scripts publish/linux/obs-scripts

- name: Add Linux launcher script
run: |
cp clipstudio.sh publish/linux/ClipStudio.sh
chmod +x publish/linux/ClipStudio.sh

- name: Bundle FFmpeg (Linux)
run: |
sudo apt-get update -qq
Expand Down
33 changes: 30 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,38 @@ Built for players who record everything but never have time to find the good mom

### Requirements

#### Windows
| Dependency | Notes |
|---|---|
| [.NET 9 Runtime](https://dot.net/download) | Required to run the app |
| [VLC media player](https://www.videolan.org/) | Required on macOS and Linux for native LibVLC libraries; on Windows, VLC libraries are bundled |
| FFmpeg | **Bundled automatically** in the installer. For development builds, install to system PATH or configure the path in Settings |
| FFmpeg | Bundled automatically in the installer |
| LibVLC | Bundled automatically in the installer |

#### macOS
| Dependency | Notes |
|---|---|
| [.NET 9 Runtime](https://dot.net/download) | Required to run the app |
| [VLC media player](https://www.videolan.org/) | Must be installed — LibVLC is loaded from the system VLC installation |
| FFmpeg | Bundled automatically in the installer |

#### Linux
| Dependency | Notes |
|---|---|
| [.NET 9 Runtime](https://dot.net/download) | Required to run the app |
| `libvlc5` | Provides the LibVLC shared library used for video playback |
| FFmpeg | Bundled automatically in the installer |

Install Linux dependencies on Debian/Ubuntu:
```bash
sudo apt-get install libvlc5
```

Use the provided launcher script instead of running the binary directly — it automatically resolves the LibVLC library path so no developer packages are required:
```bash
./ClipStudio.sh
```

> **Why not bundled on Linux?** VideoLAN does not publish an official Linux NuGet bundle (unlike Windows). Linux `.so` files are compiled against a specific distro ABI and glibc version, so there is no single binary that works across all distributions. VLC also ships hundreds of codec plugin files that would add ~100 MB to every release. Using the system VLC keeps the package small and ensures security patches flow through your distro's package manager automatically.

### Download

Expand Down Expand Up @@ -98,7 +125,7 @@ The bundled OBS script renames each replay buffer save to embed the active game

**Produced filename format:**
```
Replay 2025-03-03 22-49-45 [Apex Legends].mp4
Replay 2025-03-03 22-49-45 [Battlefield 1].mp4
```

**Installation:**
Expand Down
20 changes: 20 additions & 0 deletions clipstudio-mac.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/bin/bash
# ClipStudio macOS launcher.
#
# LaunchServices strips DYLD_* environment variables before spawning an app
# bundle process, so LibVLCSharp cannot find VLC's dylibs or plugins at
# runtime even if Core.Initialize points at the correct path.
# Running this script as CFBundleExecutable sets the required variables
# before exec-ing the real binary, preserving them across the exec call.

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"

VLC_BASE="/Applications/VLC.app/Contents/MacOS"
if [ -d "$VLC_BASE/lib" ]; then
export DYLD_LIBRARY_PATH="$VLC_BASE/lib${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}"
export VLC_PLUGIN_PATH="$VLC_BASE/plugins"
else
echo "Warning: VLC.app not found at /Applications/VLC.app. Install VLC from https://www.videolan.org/" >&2
fi

exec "$SCRIPT_DIR/ClipStudio.UI" "$@"
31 changes: 31 additions & 0 deletions clipstudio.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/bin/bash
# ClipStudio Linux launcher.
#
# LibVLCSharp requires the unversioned libvlc.so symlink, which is only
# present when the libvlc-dev package is installed. This script detects
# the versioned system library (libvlc.so.5, from the libvlc5 runtime
# package) and creates a user-local unversioned symlink on the fly, so
# the app works without any developer packages.

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"

# Only act if the unversioned symlink is absent from ldconfig's cache.
if ! ldconfig -p 2>/dev/null | grep -q "libvlc\.so$"; then
LIBVLC_VERSIONED=$(ldconfig -p 2>/dev/null \
| grep "libvlc\.so\." \
| grep -v core \
| awk '{print $NF}' \
| head -1)

if [ -n "$LIBVLC_VERSIONED" ]; then
# Use XDG_RUNTIME_DIR when available (cleared on logout); fall back to /tmp.
VLC_LINK_DIR="${XDG_RUNTIME_DIR:-/tmp}/clipstudio-vlc"
mkdir -p "$VLC_LINK_DIR"
ln -sf "$LIBVLC_VERSIONED" "$VLC_LINK_DIR/libvlc.so"
export LD_LIBRARY_PATH="$VLC_LINK_DIR${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
else
echo "Warning: libvlc5 not found. Install it with: sudo apt-get install libvlc5" >&2
fi
fi

exec "$SCRIPT_DIR/ClipStudio.UI" "$@"
17 changes: 16 additions & 1 deletion src/ClipStudio.UI/App.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,22 @@ private static IServiceProvider BuildServiceProvider()
services.AddClipStudioData(dbPath);
services.AddClipStudioApplication(settingsPath, AppDataPath);

// LibVLC — single shared instance for the lifetime of the app
// LibVLC — single shared instance for the lifetime of the app.
// On macOS the dylibs and plugins live inside the VLC.app bundle and are not
// on the dynamic linker path. Both must be pointed at explicitly before the
// first LibVLC instance is created.
if (OperatingSystem.IsMacOS())
{
var vlcBase = "/Applications/VLC.app/Contents/MacOS";
var vlcLib = Path.Combine(vlcBase, "lib");
var vlcPlugins = Path.Combine(vlcBase, "plugins");
if (Directory.Exists(vlcLib))
{
Environment.SetEnvironmentVariable("VLC_PLUGIN_PATH", vlcPlugins);
LibVLCSharp.Shared.Core.Initialize(vlcLib);
}
}

services.AddSingleton<LibVLC>(_ => new LibVLC(enableDebugLogs: false));

// Sound effects — singleton so the SoundPlayer instance is reused across calls
Expand Down
27 changes: 24 additions & 3 deletions src/ClipStudio.UI/ClipStudio.UI.csproj
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ApplicationManifest>app.manifest</ApplicationManifest>
<ApplicationManifest Condition="$([MSBuild]::IsOSPlatform('Windows'))">app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<Version>0.1.0</Version>
<AssemblyVersion>0.1.0.0</AssemblyVersion>
<FileVersion>0.1.0.0</FileVersion>
<Product>ClipStudio</Product>
<Copyright>Copyright © 2026 Claudio Bedini</Copyright>
</PropertyGroup>

<ItemGroup>
Expand All @@ -30,11 +35,27 @@
<PackageReference Include="LibVLCSharp.Avalonia" Version="3.9.6" />
<PackageReference Include="Material.Icons.Avalonia" Version="3.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.*" />
<PackageReference Include="VideoLAN.LibVLC.Windows" Version="3.0.23" />
<PackageReference Include="VideoLAN.LibVLC.Windows" Version="3.0.23" Condition="$([MSBuild]::IsOSPlatform('Windows'))" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\ClipStudio.Application\ClipStudio.Application.csproj" />
<ProjectReference Include="..\ClipStudio.Core\ClipStudio.Core.csproj" />
</ItemGroup>

<ItemGroup Condition="$(RuntimeIdentifier.StartsWith('linux'))">
<None Include="..\..\clipstudio.sh">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<TargetPath>ClipStudio.sh</TargetPath>
</None>
</ItemGroup>

<ItemGroup Condition="$(RuntimeIdentifier.StartsWith('osx'))">
<None Include="..\..\clipstudio-mac.sh">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<TargetPath>ClipStudio.sh</TargetPath>
</None>
</ItemGroup>
</Project>
27 changes: 21 additions & 6 deletions src/ClipStudio.UI/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,9 @@ internal sealed class Program
{
/// <summary>
/// GitHub repository URL used for automatic update checks.
/// Set this to <c>https://github.com/OWNER/ClipStudio</c> before publishing a release.
/// Leave <see langword="null"/> to disable background update checks entirely.
/// Set this to <see langword="null"/> to disable background update checks entirely.
/// </summary>
private static readonly string? AutoUpdateRepositoryUrl = null;
private static readonly string? AutoUpdateRepositoryUrl = "https://github.com/Phazertron/ClipStudio";

/// <summary>
/// Main entry point. Velopack's bootstrap call MUST be the very first statement so that
Expand All @@ -32,9 +31,7 @@ public static void Main(string[] args)
// (e.g. a "demo" profile for screenshots) can coexist without touching the real library.
var profileName = ParseProfileArg(args);
var folderName = profileName is not null ? $"ClipStudio_{profileName}" : "ClipStudio";
App.AppDataPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
folderName);
App.AppDataPath = ResolvePlatformAppDataPath(folderName);

// Strip --profile and its value from the args passed to Avalonia so the framework
// does not treat them as unknown arguments.
Expand Down Expand Up @@ -102,6 +99,24 @@ public static AppBuilder BuildAvaloniaApp()
.WithInterFont()
.LogToTrace();

/// <summary>
/// Returns the platform-appropriate application data directory for the given folder name.
/// Windows: %AppData%\&lt;name&gt;
/// macOS: ~/Library/Application Support/&lt;name&gt;
/// Linux: ~/.config/&lt;name&gt;
/// </summary>
private static string ResolvePlatformAppDataPath(string folderName)
{
if (OperatingSystem.IsWindows())
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), folderName);

if (OperatingSystem.IsMacOS())
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", folderName);

// Linux and other POSIX platforms
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", folderName);
}

/// <summary>
/// Returns the value of the <c>--profile</c> argument, or <see langword="null"/> if it was
/// not supplied.
Expand Down
16 changes: 15 additions & 1 deletion src/ClipStudio.UI/ViewModels/WizardSteps/FfmpegStepViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,21 @@ public Task DetectAsync()
return Task.CompletedTask;
}

// 3. Not found — ask the user
// 3. Well-known out-of-PATH locations (macOS Homebrew on Apple Silicon and Intel)
if (OperatingSystem.IsMacOS())
{
foreach (var candidate in new[] { "/opt/homebrew/bin", "/usr/local/bin" })
{
if (File.Exists(Path.Combine(candidate, exeName)))
{
DetectedFolder = candidate;
DetectionStatus = FfmpegDetectionStatus.FoundOnPath;
return Task.CompletedTask;
}
}
}

// 4. Not found — ask the user
DetectionStatus = FfmpegDetectionStatus.NotFound;
return Task.CompletedTask;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ public sealed partial class TranscriptionSetupStepViewModel : WizardStepViewMode
/// <summary>Gets a value indicating whether the model picker row should be visible.</summary>
public bool ShowModelPicker => IsEnabled;

/// <summary>Gets a value indicating whether the app is running on Linux.</summary>
public bool IsLinux => System.OperatingSystem.IsLinux();

/// <summary>Gets the command that opens a file picker to browse for a model file.</summary>
public IAsyncRelayCommand BrowseModelCommand { get; }

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System;

namespace ClipStudio.UI.ViewModels.WizardSteps;

/// <summary>
Expand All @@ -11,4 +13,7 @@ public sealed class WelcomeStepViewModel : WizardStepViewModel

/// <inheritdoc/>
public override int StepNumber => 1;

/// <summary>Gets a value indicating whether the app is running on Linux.</summary>
public bool IsLinux => OperatingSystem.IsLinux();
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,29 @@

</StackPanel>

<!-- Linux Vulkan note -->
<Border CornerRadius="6"
Background="#1AE67E22"
BorderBrush="#27E67E22"
BorderThickness="1"
Padding="12,10"
IsVisible="{Binding IsLinux}">
<StackPanel Spacing="4">
<StackPanel Orientation="Horizontal" Spacing="8">
<icons:MaterialIcon Kind="Memory" Width="16" Height="16"
Foreground="#E67E22" VerticalAlignment="Center"/>
<TextBlock Text="Linux: GPU (Vulkan) backend requires libvulkan1"
FontSize="12" FontWeight="SemiBold"
Foreground="#E67E22" VerticalAlignment="Center"/>
</StackPanel>
<TextBlock FontSize="11" Opacity="0.7" TextWrapping="Wrap"
Text="The default CPU backend works without any extra packages. For faster transcription using your GPU, install the Vulkan runtime:"/>
<TextBlock Text="sudo apt-get install libvulkan1"
FontSize="11" FontFamily="Monospace"
Opacity="0.85"/>
</StackPanel>
</Border>

<TextBlock Text="You can configure transcription at any time in Settings > Transcription."
FontSize="11"
Opacity="0.5"
Expand Down
Loading
Loading