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
6 changes: 6 additions & 0 deletions src/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -400,3 +400,9 @@ FodyWeavers.xsd

# JetBrains Rider
*.sln.iml



Sa.Media.FFmpeg/runtimes/native/
nupkgs/
.packages/
2 changes: 1 addition & 1 deletion src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@
<PackageVersion Include="Testcontainers.Minio" Version="4.10.0" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.3" />
</ItemGroup>
</Project>
</Project>
2 changes: 1 addition & 1 deletion src/Sa.HybridFileStorage.FileSystem/Setup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public static IServiceCollection AddSaFileSystemFileStorage(
{
BasePath = options.BasePath,
IsReadOnly = options.IsReadOnly,
ScopeName = options.ScopeName,
ScopeName = options.ScopeName ?? string.Empty,
StorageType = options.StorageType,
}, sp.GetService<TimeProvider>());
});
Expand Down
6 changes: 3 additions & 3 deletions src/Sa.Media.FFmpeg/FFMpegOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ namespace Sa.Media.FFmpeg;

public sealed record FFMpegOptions
{
[StringLength(500)]
[StringLength(255)]
public string? ExecutablePath { get; set; } = null;

[StringLength(500)]
[StringLength(255)]
public string? WritableDirectory { get; set; } = null;

public int? TimeoutSeconds { get; set; }

public TimeSpan? Timeout => TimeoutSeconds.HasValue
public TimeSpan? Timeout => TimeoutSeconds > 0
? TimeSpan.FromSeconds(TimeoutSeconds.Value)
: default;

Expand Down
2 changes: 1 addition & 1 deletion src/Sa.Media.FFmpeg/IFFMpegLocator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@

public interface IFFMpegLocator
{
string FindFFmpegExecutablePath(string? writableDirectory = null);
string FindFFmpegExecutablePath();
}
37 changes: 21 additions & 16 deletions src/Sa.Media.FFmpeg/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,18 @@ Interfaces:

## Example Usage

Audio Conversion to MP3
Audio Conversion

```csharp

public class AudioService(IFFMpegExecutor ffmpeg, IFFProbeExecutor ffprobe)
{
public async Task ConvertAsync(string input, string output, CancellationToken ct = default)
{
await ffmpeg.ConvertToMp3(input, output, ct);
}

public async Task<(int? channels, int? sampleRate)> GetInfoAsync(string file, CancellationToken ct = default)
{
// Получение базовой информации
return await ffprobe.GetChannelsAndSampleRate(file, ct);
}
}
var ffmpeg = Sa.Media.FFmpeg.IFFMpegExecutor.Default;

var codecs = await ffmpeg.GetCodecs();
Console.WriteLine(codecs);

await ffmpeg.ConvertToPcmS16Le(
"data/input.mp3",
"data/output.wav",
outputChannelCount: 1);
```


Expand Down Expand Up @@ -64,7 +59,7 @@ output_channel_1.wav
To see all missing dependencies:

```bash
cd bin/Debug/net10.0/runtimes/linux-x64/
cd bin/Debug/net10.0/sa/native/
ldd ffmpeg
```

Expand All @@ -80,3 +75,13 @@ On Alpine Linux:
```bash
sudo apk add lame-libs opus libvorbis
```


wsl build
```
# WSL:

dotnet nuget locals all --clear
dotnet restore -r linux-x64
dotnet build -c Debug -r linux-x64
```
152 changes: 117 additions & 35 deletions src/Sa.Media.FFmpeg/Sa.Media.FFmpeg.csproj
Original file line number Diff line number Diff line change
@@ -1,39 +1,121 @@
<Project Sdk="Microsoft.NET.Sdk">

<Import Project="../Common.NuGet.Properties.xml" />

<PropertyGroup>
<Version>0.8.1</Version>
<Description>FFmpeg wrapper</Description>
</PropertyGroup>

<ItemGroup>
<None Remove="runtimes\linux-x64\ffmpeg.zip" />
<None Remove="runtimes\win-x64\ffmpeg.zip" />
</ItemGroup>

<ItemGroup>
<Compile Include="..\Sa\Classes\IProcessExecutor.cs" Link="AsLink\IProcessExecutor.cs" />
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="Sa.Media.FFmpegTests" />
</ItemGroup>

<ItemGroup>
<EmbeddedResource Include="runtimes\linux-x64\ffmpeg.zip" />
<EmbeddedResource Include="runtimes\win-x64\ffmpeg.zip" />
</ItemGroup>

<ItemGroup>
<Folder Include="AsLink\" />
<Folder Include="Properties\" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" />
<PackageReference Include="Microsoft.Extensions.Options.DataAnnotations" />
</ItemGroup>
<Import Project="../Common.NuGet.Properties.xml" />

<PropertyGroup>
<Version>0.9.0</Version>
<Description>FFmpeg wrapper</Description>
<GeneratePathProperty>true</GeneratePathProperty>
<RuntimeIdentifiers>win-x64;win-arm64;linux-x64;linux-arm64;osx-x64</RuntimeIdentifiers>

<!-- Пути для локальной работы -->
<FfmpegSourceZipDir>$(MSBuildThisFileDirectory)sa</FfmpegSourceZipDir>
<FfmpegLocalExtractDir>$(MSBuildThisFileDirectory)sa\native</FfmpegLocalExtractDir>

<!-- 🔹 Определение RID -->
<_FfmpegCurrentRid Condition="'$(RuntimeIdentifier)' != ''">$(RuntimeIdentifier)</_FfmpegCurrentRid>
<_FfmpegCurrentRid Condition="'$(_FfmpegCurrentRid)' == ''">$(NETCoreSdkRuntimeIdentifier)</_FfmpegCurrentRid>

<!-- если macOS x64, принудительно ставим linux-x64 -->
<_FfmpegCurrentRid Condition="'$(_FfmpegCurrentRid)' == 'osx-x64'">linux-x64</_FfmpegCurrentRid>
</PropertyGroup>

<!-- 🔹 ZIP-архивы как исходники -->
<ItemGroup>
<FfmpegZipFiles Include="$(FfmpegSourceZipDir)\**\ffmpeg.zip" />
<None Include="@(FfmpegZipFiles)" Pack="false" Visible="false" />
</ItemGroup>

<Target Name="DebugRid" BeforeTargets="Build">
<Message Text="[Sa.Media.FFmpeg] === RID Debug ===" Importance="high" />
<Message Text="[Sa.Media.FFmpeg] RuntimeIdentifier: '$(RuntimeIdentifier)'" Importance="high" />
<Message Text="[Sa.Media.FFmpeg] NETCoreSdkRuntimeIdentifier: '$(NETCoreSdkRuntimeIdentifier)'" Importance="high" />
<Message Text="[Sa.Media.FFmpeg] OS: '$(OS)'" Importance="high" />
<Message Text="[Sa.Media.FFmpeg] ===============" Importance="high" />
</Target>

<!-- 🔹 Локальная распаковка: <rid>/ffmpeg.zip → sa/native/ -->
<Target Name="ExtractFfmpegLocally" BeforeTargets="BeforeBuild;CoreBuild" Condition="'$(DesignTimeBuild)' != 'true' AND '$(_FfmpegCurrentRid)' != ''">

<PropertyGroup>
<_SourceZip>$(FfmpegSourceZipDir)\$(_FfmpegCurrentRid)\ffmpeg.zip</_SourceZip>
</PropertyGroup>

<Message Text="[Sa.Media.FFmpeg] 🔍 Looking for: $(_SourceZip)" Importance="high" />

<Error Text="FFmpeg ZIP not found: $(_SourceZip). Check that sa\$(_FfmpegCurrentRid)\ffmpeg.zip exists." Condition="!Exists('$(_SourceZip)')" />

<Message Text="[Sa.Media.FFmpeg] 📦 Extracting FFmpeg for $(_FfmpegCurrentRid)..." Importance="high" Condition="Exists('$(_SourceZip)')" />

<MakeDir Directories="$(FfmpegLocalExtractDir)" />

<Unzip SourceFiles="$(_SourceZip)" DestinationFolder="$(FfmpegLocalExtractDir)" SkipUnchangedFiles="true" />

<!-- Execute bit для Unix -->
<Exec Command="chmod +x &quot;$(FfmpegLocalExtractDir)/ffmpeg&quot; &quot;$(FfmpegLocalExtractDir)/ffprobe&quot;" Condition="'$(OS)' != 'Windows_NT' AND Exists('$(FfmpegLocalExtractDir)/ffmpeg')" ContinueOnError="true" />

<Message Text="[Sa.Media.FFmpeg] ✓ Extracted: $(_SourceZip) → $(FfmpegLocalExtractDir)" Importance="high" />
</Target>


<!-- 🔹 Ключевое: делаем распакованные файлы видимыми для ProjectReference --><!--
--><!-- Используем Target-батчинг для динамического включения файлов после распаковки -->
<Target Name="IncludeExtractedFfmpegFiles" AfterTargets="ExtractFfmpegLocally" BeforeTargets="GetCopyToOutputDirectoryItems;AssignTargetPaths">

<ItemGroup Condition="Exists('$(FfmpegLocalExtractDir)')">
<!--Находим все файлы в папке распаковки-->
<_ExtractedFfmpegFiles Include="$(FfmpegLocalExtractDir)\**\*" />
</ItemGroup>

<!--Добавляем как Content с копированием - это работает для ProjectReference-->
<ItemGroup Condition="'@(_ExtractedFfmpegFiles)' != ''">
<Content Include="@(_ExtractedFfmpegFiles)" Link="sa\native\%(RecursiveDir)%(FileName)%(Extension)" CopyToOutputDirectory="PreserveNewest" Pack="false" Visible="false" />
</ItemGroup>

<Message Text="[Sa.Media.FFmpeg] 📦 Included %(_ExtractedFfmpegFiles.Filename) for copying to output" Importance="normal" Condition="'@(_ExtractedFfmpegFiles)' != ''" />
</Target>


<!--<ItemGroup Condition="'@(_ExtractedFfmpegFiles)' != ''">
<Content Include="@(_ExtractedFfmpegFiles)">
--><!-- Link задает относительный путь в папке bin потребителя --><!--
<Link>sa\native\%(RecursiveDir)%(FileName)%(Extension)</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Pack>false</Pack>
<Visible>true</Visible>
</Content>
</ItemGroup>-->


<!-- 🔹 .targets для потребителей -->
<ItemGroup>
<None Include="buildTransitive\Sa.Media.FFmpeg.targets" Pack="true" PackagePath="buildTransitive\Sa.Media.FFmpeg.targets" />
<None Include="buildTransitive\Sa.Media.FFmpeg.targets" Pack="true" PackagePath="build\Sa.Media.FFmpeg.targets" />
</ItemGroup>

<!-- 🔹 Упаковка ZIP в NuGet с RID-путём -->
<ItemGroup>
<None Include="$(FfmpegSourceZipDir)\**\ffmpeg.zip">
<Pack>true</Pack>
<PackagePath>ffmpeg</PackagePath>
<Visible>false</Visible>
</None>
</ItemGroup>

<!-- 🔹 Остальные настройки -->
<ItemGroup>
<Compile Include="..\Sa\Classes\IProcessExecutor.cs" Link="AsLink\IProcessExecutor.cs" />
<InternalsVisibleTo Include="Sa.Media.FFmpegTests" />
</ItemGroup>

<ItemGroup>
<Folder Include="Properties\" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" />
<PackageReference Include="Microsoft.Extensions.Options.DataAnnotations" />
</ItemGroup>

</Project>
2 changes: 1 addition & 1 deletion src/Sa.Media.FFmpeg/Services/FFMpegExecutorFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ private string GetExecutablePath(FFMpegOptions? mpegOptions = null)
mpegLocator ??= new FFMpegLocator();

var executablePath = mpegOptions?.ExecutablePath
?? mpegLocator.FindFFmpegExecutablePath(mpegOptions?.WritableDirectory);
?? mpegLocator.FindFFmpegExecutablePath();

if (!File.Exists(executablePath))
throw new FileNotFoundException("FFmpeg executable not found", executablePath);
Expand Down
Loading
Loading