From b93c0996bf40c203b42426b7278a334cc703fcdf Mon Sep 17 00:00:00 2001 From: XShine555 Date: Sat, 21 Mar 2026 22:23:31 +0100 Subject: [PATCH] Audio Transcoder --- .../Infrastructure/IAudioTranscoder.cs | 9 ++ Infrastructure/Audio/AudioTranscoder.cs | 92 +++++++++++++++++++ .../AudioTranscoderDependencyInjection.cs | 14 +++ .../AudioTranscoderConfiguration.cs | 22 +++++ .../ConfigurationDependencyInjection.cs | 3 +- .../InfrastructureDependencyInjection.cs | 3 + 6 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 Application/Contracts/Infrastructure/IAudioTranscoder.cs create mode 100644 Infrastructure/Audio/AudioTranscoder.cs create mode 100644 Infrastructure/Audio/AudioTranscoderDependencyInjection.cs create mode 100644 Infrastructure/Configuration/AudioTranscoderConfiguration.cs diff --git a/Application/Contracts/Infrastructure/IAudioTranscoder.cs b/Application/Contracts/Infrastructure/IAudioTranscoder.cs new file mode 100644 index 0000000..1ec7092 --- /dev/null +++ b/Application/Contracts/Infrastructure/IAudioTranscoder.cs @@ -0,0 +1,9 @@ +using Ardalis.Result; + +namespace Musify.Application.Contracts.Infrastructure +{ + public interface IAudioTranscoder + { + Task TranscodeToDash(Stream audioStream, string folderName, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/Infrastructure/Audio/AudioTranscoder.cs b/Infrastructure/Audio/AudioTranscoder.cs new file mode 100644 index 0000000..979166f --- /dev/null +++ b/Infrastructure/Audio/AudioTranscoder.cs @@ -0,0 +1,92 @@ +using Ardalis.Result; +using Microsoft.Extensions.Logging; +using Musify.Application.Contracts.Infrastructure; +using Musify.Infrastructure.Configuration; +using System.Diagnostics; + +namespace Musify.Infrastructure.Audio +{ + public class AudioTranscoder(ILogger logger, AudioTranscoderConfiguration transcoderConfiguration) + : IAudioTranscoder + { + const string FfmpegProcessName = "ffmpeg"; + + public async Task TranscodeToDash(Stream audioStream, string folderName, CancellationToken cancellationToken) + { + var workingDirectory = Path.Combine(transcoderConfiguration.Routes.WorkingDirectory, folderName); + + try + { + Directory.CreateDirectory(workingDirectory); + + using var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = FfmpegProcessName, + WorkingDirectory = workingDirectory, + Arguments = BuildDashArguments(workingDirectory), + RedirectStandardInput = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + } + }; + process.Start(); + + if (audioStream.CanSeek) + audioStream.Position = 0; + await audioStream.CopyToAsync(process.StandardInput.BaseStream, cancellationToken); + process.StandardInput.Close(); + + var errorTask = process.StandardError.ReadToEndAsync(cancellationToken); + var outputTask = process.StandardOutput.ReadToEndAsync(cancellationToken); + + using var linkedCancellationTokens = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + linkedCancellationTokens.CancelAfter(transcoderConfiguration.TranscodingTimeout); + await process.WaitForExitAsync(linkedCancellationTokens.Token); + + var standardError = await errorTask; + var standardOutput = await outputTask; + + if (process.ExitCode != 0) + { + logger.LogError("Audio transcoding to DASH format failed with exit code {ExitCode}. Standard Output: {StandardOutput}, Standard Error: {StandardError}", process.ExitCode, standardOutput, standardError); + return Result.Error($"Audio transcoding failed with exit code {process.ExitCode}. See logs for details."); + } + + logger.LogInformation("Audio transcoding to DASH format completed successfully. Uploading to storage..."); + return Result.Success(); + } + catch (Exception exception) + { + logger.LogError(exception, "An error occurred while transcoding audio to DASH format."); + return Result.Error(exception.Message); + } + } + + static string BuildDashArguments(string outputDirectory) + { + var manifestPath = Path.Combine(outputDirectory, "manifest.mpd"); + + return "-y -hide_banner -loglevel error " + + "-i pipe:0 " + + "-vn " + + "-map 0:a " + + "-c:a aac " + + "-b:a 128k " + + "-ac 2 " + + "-ar 48000 " + + "-profile:a aac_low " + + "-f dash " + + "-seg_duration 4 " + + "-streaming 1 " + + "-use_template 1 " + + "-use_timeline 0 " + + "-init_seg_name \"init-stream$RepresentationID$.m4s\" " + + "-media_seg_name \"chunk-stream$RepresentationID$-$Number$.m4s\" " + + $"\"{manifestPath}\""; + } + } +} \ No newline at end of file diff --git a/Infrastructure/Audio/AudioTranscoderDependencyInjection.cs b/Infrastructure/Audio/AudioTranscoderDependencyInjection.cs new file mode 100644 index 0000000..f7e04c2 --- /dev/null +++ b/Infrastructure/Audio/AudioTranscoderDependencyInjection.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.DependencyInjection; +using Musify.Application.Contracts.Infrastructure; + +namespace Musify.Infrastructure.Audio +{ + public static class AudioTranscoderDependencyInjection + { + public static IServiceCollection AddAudioTranscoder(this IServiceCollection serviceDescriptors) + { + serviceDescriptors.AddScoped(); + return serviceDescriptors; + } + } +} \ No newline at end of file diff --git a/Infrastructure/Configuration/AudioTranscoderConfiguration.cs b/Infrastructure/Configuration/AudioTranscoderConfiguration.cs new file mode 100644 index 0000000..c19ecca --- /dev/null +++ b/Infrastructure/Configuration/AudioTranscoderConfiguration.cs @@ -0,0 +1,22 @@ +using System.ComponentModel.DataAnnotations; + +namespace Musify.Infrastructure.Configuration +{ +#pragma warning disable CS8618 + public class AudioTranscoderConfiguration + { + public const string SectionName = "AudioTranscoder"; + + [Required] + public AudioTranscoderRoutes Routes { get; set; } = new AudioTranscoderRoutes(); + + [Required] + public TimeSpan TranscodingTimeout { get; set; } = TimeSpan.FromMinutes(5); + } + + public class AudioTranscoderRoutes + { + [Required] + public string WorkingDirectory { get; set; } + } +} \ No newline at end of file diff --git a/Infrastructure/Configuration/ConfigurationDependencyInjection.cs b/Infrastructure/Configuration/ConfigurationDependencyInjection.cs index 10a6be7..f275aa4 100644 --- a/Infrastructure/Configuration/ConfigurationDependencyInjection.cs +++ b/Infrastructure/Configuration/ConfigurationDependencyInjection.cs @@ -13,7 +13,8 @@ namespace Musify.Infrastructure.Configuration .AddValidatedConfiguration(configuration, KeycloakConfiguration.SectionName) .AddValidatedConfiguration(configuration, InfrastructureStorageConfiguration.SectionName) .AddValidatedConfiguration(configuration, MessagingConfiguration.SectionName) - .AddValidatedConfiguration(configuration, OpenTelemetryConfiguration.SectionName); + .AddValidatedConfiguration(configuration, OpenTelemetryConfiguration.SectionName) + .AddValidatedConfiguration(configuration, AudioTranscoderConfiguration.SectionName); return serviceDescriptors; } diff --git a/Infrastructure/InfrastructureDependencyInjection.cs b/Infrastructure/InfrastructureDependencyInjection.cs index 8923781..72263f8 100644 --- a/Infrastructure/InfrastructureDependencyInjection.cs +++ b/Infrastructure/InfrastructureDependencyInjection.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Musify.Application.Contracts.Infrastructure; +using Musify.Infrastructure.Audio; using Musify.Infrastructure.Configuration; using Musify.Infrastructure.Identity; using Musify.Infrastructure.Messaging; @@ -21,6 +22,7 @@ namespace Musify.Infrastructure .AddKeycloakService(configuration) .AddStorageHandler() .AddPictureHandler() + .AddAudioTranscoder() .AddInfrastructureOpenTelemetry() .AddMassTransitClient() .AddScoped() @@ -36,6 +38,7 @@ namespace Musify.Infrastructure .AddDatabase(configuration) .AddStorageHandler() .AddPictureHandler() + .AddAudioTranscoder() .AddInfrastructureOpenTelemetry() .AddMassTransitConsumers() .AddScoped()