diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 325319d..35e97b7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: @@ -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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index adeff32..bf27d0b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 }} \ @@ -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 @@ -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" \ @@ -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 }} \ @@ -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 diff --git a/README.md b/README.md index 21a6049..293ffb4 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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:** diff --git a/clipstudio-mac.sh b/clipstudio-mac.sh new file mode 100644 index 0000000..eaa4f9a --- /dev/null +++ b/clipstudio-mac.sh @@ -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" "$@" diff --git a/clipstudio.sh b/clipstudio.sh new file mode 100644 index 0000000..84244ff --- /dev/null +++ b/clipstudio.sh @@ -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" "$@" diff --git a/src/ClipStudio.UI/App.axaml.cs b/src/ClipStudio.UI/App.axaml.cs index 46f2949..305dc72 100644 --- a/src/ClipStudio.UI/App.axaml.cs +++ b/src/ClipStudio.UI/App.axaml.cs @@ -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(_ => new LibVLC(enableDebugLogs: false)); // Sound effects — singleton so the SoundPlayer instance is reused across calls diff --git a/src/ClipStudio.UI/ClipStudio.UI.csproj b/src/ClipStudio.UI/ClipStudio.UI.csproj index aedcbdf..7996fb9 100644 --- a/src/ClipStudio.UI/ClipStudio.UI.csproj +++ b/src/ClipStudio.UI/ClipStudio.UI.csproj @@ -1,10 +1,15 @@  - WinExe + Exe net9.0 enable - app.manifest + app.manifest true + 0.1.0 + 0.1.0.0 + 0.1.0.0 + ClipStudio + Copyright © 2026 Claudio Bedini @@ -30,11 +35,27 @@ - + + + + + PreserveNewest + PreserveNewest + ClipStudio.sh + + + + + + PreserveNewest + PreserveNewest + ClipStudio.sh + + diff --git a/src/ClipStudio.UI/Program.cs b/src/ClipStudio.UI/Program.cs index 8878fea..32049a4 100644 --- a/src/ClipStudio.UI/Program.cs +++ b/src/ClipStudio.UI/Program.cs @@ -14,10 +14,9 @@ internal sealed class Program { /// /// GitHub repository URL used for automatic update checks. - /// Set this to https://github.com/OWNER/ClipStudio before publishing a release. - /// Leave to disable background update checks entirely. + /// Set this to to disable background update checks entirely. /// - private static readonly string? AutoUpdateRepositoryUrl = null; + private static readonly string? AutoUpdateRepositoryUrl = "https://github.com/Phazertron/ClipStudio"; /// /// Main entry point. Velopack's bootstrap call MUST be the very first statement so that @@ -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. @@ -102,6 +99,24 @@ public static AppBuilder BuildAvaloniaApp() .WithInterFont() .LogToTrace(); + /// + /// Returns the platform-appropriate application data directory for the given folder name. + /// Windows: %AppData%\<name> + /// macOS: ~/Library/Application Support/<name> + /// Linux: ~/.config/<name> + /// + 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); + } + /// /// Returns the value of the --profile argument, or if it was /// not supplied. diff --git a/src/ClipStudio.UI/ViewModels/WizardSteps/FfmpegStepViewModel.cs b/src/ClipStudio.UI/ViewModels/WizardSteps/FfmpegStepViewModel.cs index 5a1ad0b..4792917 100644 --- a/src/ClipStudio.UI/ViewModels/WizardSteps/FfmpegStepViewModel.cs +++ b/src/ClipStudio.UI/ViewModels/WizardSteps/FfmpegStepViewModel.cs @@ -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; } diff --git a/src/ClipStudio.UI/ViewModels/WizardSteps/TranscriptionSetupStepViewModel.cs b/src/ClipStudio.UI/ViewModels/WizardSteps/TranscriptionSetupStepViewModel.cs index 6c6245a..0ccab45 100644 --- a/src/ClipStudio.UI/ViewModels/WizardSteps/TranscriptionSetupStepViewModel.cs +++ b/src/ClipStudio.UI/ViewModels/WizardSteps/TranscriptionSetupStepViewModel.cs @@ -40,6 +40,9 @@ public sealed partial class TranscriptionSetupStepViewModel : WizardStepViewMode /// Gets a value indicating whether the model picker row should be visible. public bool ShowModelPicker => IsEnabled; + /// Gets a value indicating whether the app is running on Linux. + public bool IsLinux => System.OperatingSystem.IsLinux(); + /// Gets the command that opens a file picker to browse for a model file. public IAsyncRelayCommand BrowseModelCommand { get; } diff --git a/src/ClipStudio.UI/ViewModels/WizardSteps/WelcomeStepViewModel.cs b/src/ClipStudio.UI/ViewModels/WizardSteps/WelcomeStepViewModel.cs index 73d883f..d5170cd 100644 --- a/src/ClipStudio.UI/ViewModels/WizardSteps/WelcomeStepViewModel.cs +++ b/src/ClipStudio.UI/ViewModels/WizardSteps/WelcomeStepViewModel.cs @@ -1,3 +1,5 @@ +using System; + namespace ClipStudio.UI.ViewModels.WizardSteps; /// @@ -11,4 +13,7 @@ public sealed class WelcomeStepViewModel : WizardStepViewModel /// public override int StepNumber => 1; + + /// Gets a value indicating whether the app is running on Linux. + public bool IsLinux => OperatingSystem.IsLinux(); } diff --git a/src/ClipStudio.UI/Views/WizardSteps/TranscriptionSetupStepView.axaml b/src/ClipStudio.UI/Views/WizardSteps/TranscriptionSetupStepView.axaml index 4a00675..52bd8e3 100644 --- a/src/ClipStudio.UI/Views/WizardSteps/TranscriptionSetupStepView.axaml +++ b/src/ClipStudio.UI/Views/WizardSteps/TranscriptionSetupStepView.axaml @@ -60,6 +60,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +