Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
114 commits
Select commit Hold shift + click to select a range
53c63b8
begin FFmpeg support
Axwabo Feb 10, 2026
536a718
Linux installer
Axwabo Feb 10, 2026
d674a0d
use FFmpegSL in audio processor
Axwabo Feb 10, 2026
5f57de3
use UnityWebRequest
Axwabo Feb 10, 2026
ab84663
fix read count calculation
Axwabo Feb 10, 2026
0595a00
save ffmpeg path on install
Axwabo Feb 10, 2026
51741a4
goofy ahh async provider
Axwabo Feb 21, 2026
2c008f4
improve installer
Axwabo Feb 21, 2026
0bdefce
download progress tracking
Axwabo Feb 21, 2026
684f836
copy existing installation if available
Axwabo Feb 21, 2026
5b902f2
ffmpeg windows installer
Axwabo Feb 21, 2026
d52e107
improve existing folder handling
Axwabo Feb 21, 2026
3a2c837
chcek for multiple locations
Axwabo Feb 21, 2026
17a5513
error if FFmpeg is not installed
Axwabo Feb 21, 2026
7cfba5a
logger global using + restructure + native error codes
Axwabo Feb 21, 2026
905402e
override config option
Axwabo Feb 21, 2026
01160bf
refactor error handling
Axwabo Feb 21, 2026
d082ae6
improve startup error log + chmod command
Axwabo Feb 22, 2026
493890e
wait for exit in IsInstalled
Axwabo Feb 22, 2026
a8a6336
timeout
Axwabo Feb 22, 2026
844bdcf
kill if no main window
Axwabo Feb 22, 2026
b34270e
Revert "kill if no main window"
Axwabo Feb 22, 2026
88372e5
Revert "timeout"
Axwabo Feb 22, 2026
8cf7eeb
kill on dispose
Axwabo Feb 22, 2026
33765bc
share package properties
Axwabo Feb 22, 2026
368978f
begin FFmpeg docs
Axwabo Feb 22, 2026
108561e
FFmpegSL docs
Axwabo Feb 22, 2026
309d7e9
PSI ctor
Axwabo Feb 22, 2026
228a02c
FFmpegArgumentsBuilder
Axwabo Feb 25, 2026
7733997
ShortClipCacheExtensions + FFmpegArgumentsBuilder refactor
Axwabo Feb 25, 2026
62f8627
refactor
Axwabo Feb 25, 2026
4daf07c
FFmpegArguments
Axwabo Feb 25, 2026
1439c19
FFmpegArguments building
Axwabo Feb 25, 2026
854c1fb
more utils
Axwabo Feb 25, 2026
45c32bb
update FFmpeg README
Axwabo Feb 25, 2026
22085ac
bump package version
Axwabo Feb 25, 2026
d366c1b
FFmpegArguments loop
Axwabo Feb 25, 2026
8510b36
catch network error in installer
Axwabo Feb 25, 2026
c85b6ce
catch exception in FFmpegSL.Dispose
Axwabo Feb 25, 2026
1f2eab1
begin ShortClipCache async
Axwabo Feb 25, 2026
879a2d0
don't use indexer with AsSpan
Axwabo Feb 25, 2026
42e277f
rearrange partials + ExitCode prop
Axwabo Feb 26, 2026
3441bfe
more global usings
Axwabo Feb 26, 2026
badb59b
rename FFmpegStartException + partial sync processor
Axwabo Feb 26, 2026
0ea0fb9
async buffered refactor
Axwabo Feb 26, 2026
8ad3c58
attempt async
Axwabo Feb 26, 2026
8c65106
Revert "attempt async"
Axwabo Feb 26, 2026
95e1142
refactor AsyncBufferedFFmpegAudioProcessor
Axwabo Feb 26, 2026
a6bd0d5
extract superclass
Axwabo Feb 26, 2026
7b630ad
fix resource handling & invocation
Axwabo Feb 26, 2026
b911f50
refactor async FFmpeg creation
Axwabo Feb 26, 2026
8c67780
pre-validate ffmpeg arguments + set buffering state
Axwabo Feb 26, 2026
da15f85
rename Run
Axwabo Feb 26, 2026
a10ef94
rename StreamBasedFFmpegProcessor
Axwabo Feb 26, 2026
e9d3984
slightly optimize buffer loop
Axwabo Feb 26, 2026
12e1b2e
prevent non-float-size writes + PadIfFallingBehind
Axwabo Feb 26, 2026
6de0fee
rename to AutoRefill
Axwabo Feb 26, 2026
5c3c333
rename to Endless + use Endless in loop
Axwabo Feb 26, 2026
e98df20
fix incorrect graceful termination location
Axwabo Feb 28, 2026
5aada7f
private processor constructors
Axwabo Feb 28, 2026
38971a2
IFFmpegWrapper
Axwabo Feb 28, 2026
2edf353
overwriting instead of overriding
Axwabo Feb 28, 2026
7ea299f
stream overloads
Axwabo Feb 28, 2026
937da57
IsDisposed property
Axwabo Feb 28, 2026
6e857c6
docs + rename StartRaw + reorder async state enums
Axwabo Mar 1, 2026
c905752
fix System.ValueTuple + bundle FFmpeg
Axwabo Mar 1, 2026
dfe0c07
check for null or whitespace before setting FFmpeg path
Axwabo Mar 3, 2026
de19743
SCC extensions progress + docs progress
Axwabo Mar 4, 2026
ac8df94
partial FFmpeg args docs + timespan format
Axwabo Mar 4, 2026
21a056f
docs progress + FFmpegArguments modifications
Axwabo Mar 5, 2026
29151fd
slight cleanup
Axwabo Mar 5, 2026
73a1558
add to plugins when embedded
Axwabo Mar 5, 2026
42b8d90
close standard input in StreamBasedFFmpegAudioProcessor
Axwabo Mar 7, 2026
a47bcf2
remove async
Axwabo Mar 7, 2026
4bbb00b
dispose FFmpeg if disposed on another thread
Axwabo Mar 8, 2026
32dd564
short clip cache reading & docs
Axwabo Mar 8, 2026
3bf8724
begin args xml docs
Axwabo Mar 8, 2026
79e3380
docs progress
Axwabo Mar 10, 2026
e9ce539
complete docs
Axwabo Mar 10, 2026
6a69312
update ATTRIBUTIONS.md
Axwabo Mar 10, 2026
41865d5
clearer grammar
Axwabo Mar 10, 2026
6462800
remove 3-param UseFFmpeg overload
Axwabo Mar 10, 2026
e2d8e74
update READMEs
Axwabo Mar 10, 2026
43546af
update README.md
Axwabo Mar 10, 2026
13e90bb
capacity remarks
Axwabo Mar 10, 2026
8048158
remove the
Axwabo Mar 11, 2026
a83bdd7
exception doc include + fix some docs
Axwabo Mar 11, 2026
d82d70e
IsExternalInit in FFmpeg module
Axwabo Mar 12, 2026
0bd4040
>= in buffer loop
Axwabo Mar 14, 2026
7f8ba37
begin cache
Axwabo Mar 14, 2026
3ef41ee
overwrite file
Axwabo Mar 14, 2026
a36896b
bump version + save to plugins instead
Axwabo Mar 14, 2026
5fc29f4
cancellation + AudioCacheBase
Axwabo Mar 14, 2026
58ad2b5
append .path instead of replacing
Axwabo Mar 14, 2026
68bf6c1
cache error extensions
Axwabo Mar 14, 2026
dcb299e
begin cache docs
Axwabo Mar 15, 2026
7786f0b
slight refactor + docs progress
Axwabo Mar 19, 2026
4615101
error docs
Axwabo Mar 20, 2026
e27224d
cache docs progress
Axwabo Mar 20, 2026
fb2158f
docs progress
Axwabo Mar 20, 2026
e8adde6
docs progress
Axwabo Mar 22, 2026
49c369e
ignore video track + complete docs
Axwabo Mar 27, 2026
8b2eaf0
rename CacheAllIfUpdatedAsync
Axwabo Mar 29, 2026
c1c614e
rename Output + public cache methods
Axwabo Mar 28, 2026
197fd94
make arguments template public
Axwabo Mar 29, 2026
5cc9df1
TryGetPath remarks
Axwabo Apr 1, 2026
9218b96
explicit error name
Axwabo Apr 1, 2026
0350af8
fix FFmpegInstaller
Axwabo Apr 1, 2026
847c148
always redirect standard input
Axwabo Apr 4, 2026
6af089f
use env
Axwabo Apr 4, 2026
791e4f3
fix env location
Axwabo Apr 4, 2026
ac94207
update error messages
Axwabo Apr 4, 2026
72a7d16
convert to full path in SFC::GetKey
Axwabo Apr 4, 2026
9921e05
fix README installation indent
Axwabo Apr 4, 2026
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
13 changes: 13 additions & 0 deletions ATTRIBUTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,16 @@ MPEG 1 & 2 Decoder for Layers 1, 2, & 3
https://github.com/naudio/NLayer

License: MIT

# FFmpeg

> [!NOTE]
> While SecretLabNAudio does not ship, nor does it link its libraries with FFmpeg,
> it starts FFmpeg processes to provide utilities.
> SecretLabNAudio contains an installer to download FFmpeg.

A complete, cross-platform solution to record, convert and stream audio and video.

https://ffmpeg.org/

License: LGPL-2.1
15 changes: 14 additions & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
<Project>

<PropertyGroup>
<TargetFramework>net48</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>14</LangVersion>
<PlatformTarget>x64</PlatformTarget>
<DisableImplicitFrameworkReferences>true</DisableImplicitFrameworkReferences>
<Version>2.0.0</Version>
<PackageVersion>2.0.0-alpha5</PackageVersion>
</PropertyGroup>

<PropertyGroup>
<Authors>Axwabo</Authors>
<Company>Axwabo</Company>

<PackageVersion>2.0.0-beta2</PackageVersion>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageLicenseExpression>LGPL-3.0-only</PackageLicenseExpression>

<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/Axwabo/SecretLabNAudio</RepositoryUrl>
</PropertyGroup>

<ItemGroup>
Expand Down Expand Up @@ -42,4 +54,5 @@
<ItemGroup Condition="$(ProjectName) != 'SecretLabNAudio.Core'">
<ProjectReference Include="../SecretLabNAudio.Core/SecretLabNAudio.Core.csproj" />
</ItemGroup>

</Project>
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@

<PackageVersion Include="NVorbis" Version="0.10.5" />
<PackageVersion Include="NAudio.Vorbis" Version="1.5.0" />
<PackageVersion Include="System.ValueTuple" Version="4.5.0" />
</ItemGroup>
</Project>
17 changes: 13 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ This library has a number of open-source dependencies. See [Attributions](ATTRIB
- SpeakerToy pooling
- Automatic reader resolution by file type
- Cache for short audio clips
- FFmpeg-based audio processing for (almost) all formats, even over the network
- Windows-only Media Foundation support for a wider range of formats, and decoding over the network

> [!TIP]
Expand Down Expand Up @@ -51,30 +52,38 @@ This library has a number of open-source dependencies. See [Attributions](ATTRIB
3. Extract the necessary DLLs from the `bin/` directory
- See the [table below](#modules) for what you need
- Place dependencies into the **dependencies** directory
- Linux: `~/.config/SCP Secret Laboratory/LabAPI/dependencies/<port>/`
- Windows: `%appdata%/SCP Secret Laboratory/LabAPI/dependencies/<port>/`
- Linux: `~/.config/SCP Secret Laboratory/LabAPI/dependencies/<port>/`
- Windows: `%appdata%/SCP Secret Laboratory/LabAPI/dependencies/<port>/`
- Place plugins into the **plugins** directory
- Linux: `~/.config/SCP Secret Laboratory/LabAPI/plugins/<port>/`
- Windows: `%appdata%/SCP Secret Laboratory/LabAPI/plugins/<port>/`
- Linux: `~/.config/SCP Secret Laboratory/LabAPI/plugins/<port>/`
- Windows: `%appdata%/SCP Secret Laboratory/LabAPI/plugins/<port>/`
4. Restart the server

### Modules

To support reading from some file formats, install the modules you need.

FFmpeg supports effectively all formats at the cost of running as a separate process.
The FFmpeg module's APIs must be invoked separately.

| Usage | Plugin | Dependencies |
|--------------|------------------------------------|-----------------------------------------------|
| **required** | (none) | `SecretLabNAudio.Core` `NAudio.Core` |
| mp3 | `SecretLabNAudio.NLayer` | `NLayer` `NLayer.NAudioSupport` |
| ogg | `SecretLabNAudio.NVorbis` | `NVorbis` `NAudio.Vorbis` `System.ValueTuple` |
| most formats | `SecretLabNAudio.MediaFoundation`* | `NAudio.Wasapi`* |
| FFmpeg | `SecretLabNAudio.FFmpeg`** | (none) |

> [!NOTE]
> *MediaFoundation is only available on Windows.
>
> **FFmpeg itself is not shipped with SecretLabNAudio.
> See the [wiki](https://github.com/Axwabo/SecretLabNAudio/wiki/FFmpeg-Installation) on how to install it.

## Development

Simply install the `SecretLabNAudio.Core` package from NuGet.
You can also add the `SecretLabNAudio.FFmpeg` package, which references the former one.

Manual installation:

Expand Down
2 changes: 1 addition & 1 deletion SecretLabNAudio.Core/AudioPlayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ private void ProcessPacket()
_remainingTime -= PacketDuration;
}

OutputMonitor?.OnRead(ReadBuffer.AsSpan()[..read]);
OutputMonitor?.OnRead(ReadBuffer.AsSpan(0, read));
if (SendEngine == null)
return;
if (MasterAmplification is not 1f)
Expand Down
18 changes: 13 additions & 5 deletions SecretLabNAudio.Core/FileReading/ClipName.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ public readonly record struct ClipName(string Original, bool TrimExtension = tru
/// <returns>A new <see cref="ClipName"/>.</returns>
public static implicit operator ClipName(string original) => new(original);

/// <summary>
/// Converts this name to its final form.
/// </summary>
/// <returns>The extension removed if <see cref="TrimExtension"/> is true, <see cref="Original"/> otherwise.</returns>
public override string ToString() => TrimExtension
? Path.ChangeExtension(Original, null)
: Original;

/// <summary>
/// Converts a name and trim flag tuple to a <see cref="ClipName"/>.
/// </summary>
Expand All @@ -24,11 +32,11 @@ public static implicit operator ClipName((string Original, bool TrimExtension) t
=> new(tuple.Original, tuple.TrimExtension);

/// <summary>
/// Converts this name to its final form.
/// Converts the path to a <see cref="ClipName"/> based on the file's name.
/// </summary>
/// <returns>The extension removed if <see cref="TrimExtension"/> is true, <see cref="Original"/> otherwise.</returns>
public override string ToString() => TrimExtension
? Path.ChangeExtension(Original, null)
: Original;
/// <param name="path">The path (absolute or relative) to convert.</param>
/// <param name="trimExtension">The value of <see cref="TrimExtension"/> in the result.</param>
/// <returns>A new <see cref="ClipName"/> corresponding to the file name.</returns>
public static ClipName FromPath(string path, bool trimExtension = true) => new(Path.GetFileName(path), trimExtension);

}
4 changes: 2 additions & 2 deletions SecretLabNAudio.Core/Processors/Mixer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,8 @@ private void ThrowIfIncompatible(ISampleProvider input, bool isOwned)
public int Read(float[] buffer, int offset, int count)
{
_readBuffer = BufferHelpers.Ensure(_readBuffer, count);
var readSpan = _readBuffer.AsSpan()[..count];
var targetSpan = buffer.AsSpan()[offset..(offset + count)];
var readSpan = _readBuffer.AsSpan(0, count);
var targetSpan = buffer.AsSpan(offset, count);
targetSpan.Clear();
var total = 0;
for (var i = _inputs.Count - 1; i >= 0; i--)
Expand Down
10 changes: 2 additions & 8 deletions SecretLabNAudio.Core/SecretLabNAudio.Core.csproj
Original file line number Diff line number Diff line change
@@ -1,26 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
</PropertyGroup>

<PropertyGroup>
<Authors>Axwabo</Authors>
<Company>Axwabo</Company>
<Product>SecretLabNAudio.Core</Product>

<PackageId>SecretLabNAudio.Core</PackageId>
<PackageDescription>Advanced audio player API for SCP: Secret Laboratory using NAudio</PackageDescription>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageLicenseExpression>LGPL-3.0-only</PackageLicenseExpression>
<PackageTags>SCP:SL LabAPI NAudio</PackageTags>
<PackageProjectUrl>https://nuget.org/packages/SecretLabNAudio.Core</PackageProjectUrl>

<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/Axwabo/SecretLabNAudio</RepositoryUrl>
</PropertyGroup>

<ItemGroup>
<None Include="../README.md" Pack="true" PackagePath="/" />
</ItemGroup>

</Project>
2 changes: 1 addition & 1 deletion SecretLabNAudio.Core/XmlDocs/Clips.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
A
<see cref="T:SecretLabNAudio.Core.Providers.RawSourceSampleProvider" />
if the clip was added to the cache.
If the file doesn't exist, no <see cref="T:SecretLabNAudio.Core.FileReading.IAudioReaderFactory">factory</see> was registered for the type, or no
Null if the file doesn't exist, no <see cref="T:SecretLabNAudio.Core.FileReading.IAudioReaderFactory">factory</see> was registered for the type, or no
<see cref="T:NAudio.Wave.WaveStream" />
was returned.
</returns>
Expand Down
88 changes: 88 additions & 0 deletions SecretLabNAudio.FFmpeg/Caches/AudioCacheBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using SecretLabNAudio.FFmpeg.Extensions;

namespace SecretLabNAudio.FFmpeg.Caches;

/// <summary>
/// A base class for caching optimized audio files.
/// </summary>
/// <typeparam name="TSource">The type of input to accept from callers.</typeparam>
/// <typeparam name="TKey">The type of key to generate the file path with.</typeparam>
public abstract class AudioCacheBase<TSource, TKey>
{

/// <summary>
/// Initializes a new cache, and creates the directory if necessary.
/// </summary>
/// <param name="folder">The directory to save files to.</param>
protected AudioCacheBase(string folder)
{
Folder = folder;
Directory.CreateDirectory(folder);
}

/// <summary>
/// Initializes a new cache, and creates the directory if necessary.
/// </summary>
/// <param name="directoryInfo">The directory to save files to.</param>
protected AudioCacheBase(DirectoryInfo directoryInfo) : this(directoryInfo.FullName)
{
}

/// <summary>
/// The directory to save files to.
/// </summary>
public string Folder { get; }

/// <summary>
/// Gets the key corresponding to the source.
/// </summary>
/// <param name="source">The source to convert.</param>
/// <returns>The key associated with the source.</returns>
public abstract TKey GetKey(TSource source);

/// <summary>
/// Gets fully qualified output path given a key and an optimization target.
/// </summary>
/// <param name="key">The key to save by.</param>
/// <param name="optimizeFor">What to optimize for.</param>
/// <returns>A fully qualified path to the cached file.</returns>
public string GetOutput(TKey key, OptimizeFor optimizeFor) => Path.Combine(Folder, $"{key}.{optimizeFor.Extension}");

/// <summary>
/// Asynchronously starts and waits for FFmpeg to perform caching.
/// </summary>
/// <param name="source">The object to save by.</param>
/// <param name="optimizeFor">What to optimize for.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>An <see cref="Awaitable"/> representing the asynchronous operation.</returns>
public abstract Awaitable<SaveCacheResult> CacheAsync(TSource source, OptimizeFor optimizeFor, CancellationToken cancellationToken = default);

/// <summary>
/// Attempts to get the cached path of a source.
/// </summary>
/// <param name="source">The object to find the value by.</param>
/// <param name="cachedPath">The fully qualified path if a cached file was found, null otherwise.</param>
/// <returns>Whether a cached file was found.</returns>
/// <remarks><see cref="OptimizeFor.ReadingSpeed"/> is checked first, then <see cref="OptimizeFor.FileSize"/>.</remarks>
public virtual bool TryGetPath(TSource source, [NotNullWhen(true)] out string? cachedPath)
{
var key = GetKey(source);
var speed = GetOutput(key, OptimizeFor.ReadingSpeed);
if (File.Exists(speed))
{
cachedPath = speed;
return true;
}

var size = GetOutput(key, OptimizeFor.FileSize);
if (File.Exists(size))
{
cachedPath = size;
return true;
}

cachedPath = null;
return false;
}

}
19 changes: 19 additions & 0 deletions SecretLabNAudio.FFmpeg/Caches/OptimizeFor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace SecretLabNAudio.FFmpeg.Caches;

/// <summary>
/// Represents values indicating a cached audio file's type.
/// </summary>
public enum OptimizeFor
{

/// <summary>
/// Optimize for reading performance (WAV).
/// </summary>
ReadingSpeed,

/// <summary>
/// Optimize for a smaller file size (Ogg Vorbis).
/// </summary>
FileSize

}
63 changes: 63 additions & 0 deletions SecretLabNAudio.FFmpeg/Caches/SaveCacheError.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
namespace SecretLabNAudio.FFmpeg.Caches;

/// <summary>
/// Represents error states that can occur while saving an audio file to a cache.
/// </summary>
public abstract record SaveCacheError
{

/// <summary>
/// The error returned when the <see cref="CancellationToken"/> signals cancellation.
/// </summary>
public static SaveCacheError Canceled { get; } = new CanceledError();

/// <summary>
/// Creates a <see cref="FFmpegStartupError"/> based on the error code.
/// </summary>
/// <param name="errorCode">The error code to convert.</param>
/// <returns>A new <see cref="FFmpegStartupError"/> as a <see cref="SaveCacheError"/>.</returns>
public static implicit operator SaveCacheError(NativeErrorCode errorCode) => new FFmpegStartupError(errorCode);

/// <summary>
/// Creates an <see cref="ExceptionError"/> from the exception.
/// </summary>
/// <param name="exception">The exception to encapsulate.</param>
/// <returns>A new <see cref="ExceptionError"/> as a <see cref="SaveCacheError"/>.</returns>
public static implicit operator SaveCacheError(Exception exception) => new ExceptionError(exception);

}

/// <summary>
/// The error representing an invalid input argument.
/// </summary>
/// <param name="Source">The source that was provided.</param>
public sealed record InvalidInputError(string? Source) : SaveCacheError;

/// <summary>
/// The error representing a file that was not found.
/// </summary>
/// <param name="Path">The provided file path.</param>
public sealed record FileNotFoundError(string Path) : SaveCacheError;

/// <summary>
/// The error representing an FFmpeg startup failure.
/// </summary>
/// <param name="ErrorCode">The <see cref="System.ComponentModel.Win32Exception.NativeErrorCode"/> that was caught while starting FFmpeg.</param>
public sealed record FFmpegStartupError(NativeErrorCode ErrorCode) : SaveCacheError;

/// <summary>
/// The error representing an error outputted by FFmpeg during its runtime.
/// </summary>
/// <param name="ErrorMessage">The full output of the standard error.</param>
public sealed record FFmpegRuntimeError(string ErrorMessage) : SaveCacheError;

/// <summary>
/// The error representing the cancellation signaled by a <see cref="CancellationToken"/>.
/// </summary>
public sealed record CanceledError : SaveCacheError;

/// <summary>
/// The error encapsulating an <see cref="System.Exception"/>.
/// </summary>
/// <param name="Exception">The exception that was caught.</param>
public sealed record ExceptionError(Exception Exception) : SaveCacheError;
Loading