Implement preferred audio languages feature: update site settings to include preferred audio languages, modify related data structures and API endpoints, and enhance media processing to select audio tracks based on user preferences. Update frontend components for managing and displaying these settings.
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
using TeleWave.Application.Media;
|
||||
|
||||
namespace TeleWave.Infrastructure.Media;
|
||||
|
||||
@@ -12,6 +14,7 @@ namespace TeleWave.Infrastructure.Media;
|
||||
/// </summary>
|
||||
public sealed class FfmpegMediaProcessor(
|
||||
MediaPathResolver paths,
|
||||
IServiceScopeFactory scopeFactory,
|
||||
IOptions<StorageOptions> storageOptions,
|
||||
IOptions<MediaOptions> mediaOptions
|
||||
) : IMediaProcessor
|
||||
@@ -42,8 +45,12 @@ public sealed class FfmpegMediaProcessor(
|
||||
Directory.Delete(assetDir, recursive: true);
|
||||
Directory.CreateDirectory(assetDir);
|
||||
|
||||
// Выбор аудиодорожки по предпочитаемым языкам (настройка сайта, иначе дефолт из конфига).
|
||||
var preferred = await GetPreferredLanguagesAsync(cancellationToken);
|
||||
var audioTrack = AudioTrackSelector.Select(probe.AudioLanguages, preferred);
|
||||
|
||||
var (outWidth, outHeight) = ScaleDown(probe.Width, probe.Height);
|
||||
var args = BuildFfmpegArgs(originalPath, assetDir, segmentSeconds, target, pad);
|
||||
var args = BuildFfmpegArgs(originalPath, assetDir, segmentSeconds, target, pad, audioTrack);
|
||||
|
||||
var result = await ProcessRunner.RunAsync(
|
||||
_media.FfmpegPath,
|
||||
@@ -82,7 +89,8 @@ public sealed class FfmpegMediaProcessor(
|
||||
string assetDir,
|
||||
int segmentSeconds,
|
||||
double target,
|
||||
double pad
|
||||
double pad,
|
||||
int? audioTrack
|
||||
)
|
||||
{
|
||||
var padding = pad > 0.001;
|
||||
@@ -97,6 +105,21 @@ public sealed class FfmpegMediaProcessor(
|
||||
"-y",
|
||||
"-i",
|
||||
input,
|
||||
};
|
||||
|
||||
// Явный выбор дорожки по предпочитаемому языку: маппим видео + конкретную аудиодорожку.
|
||||
// Без выбора (audioTrack == null) — не маппим, оставляя дефолтную эвристику ffmpeg (как раньше).
|
||||
if (audioTrack is { } track)
|
||||
{
|
||||
args.AddRange([
|
||||
"-map",
|
||||
"0:v:0",
|
||||
"-map",
|
||||
$"0:a:{track.ToString(CultureInfo.InvariantCulture)}",
|
||||
]);
|
||||
}
|
||||
|
||||
args.AddRange([
|
||||
"-threads",
|
||||
_media.TranscodeThreads.ToString(CultureInfo.InvariantCulture),
|
||||
"-c:v",
|
||||
@@ -123,7 +146,7 @@ public sealed class FfmpegMediaProcessor(
|
||||
"2",
|
||||
"-ar",
|
||||
"48000",
|
||||
};
|
||||
]);
|
||||
|
||||
// Аудиофильтры: нормализация громкости (EBU R128, чтобы все ролики звучали одинаково) и,
|
||||
// при добивке хвоста, тишина apad. Порядок: сначала loudnorm по реальному звуку, потом apad.
|
||||
@@ -197,18 +220,30 @@ public sealed class FfmpegMediaProcessor(
|
||||
int width = 0,
|
||||
height = 0;
|
||||
var hasVideo = false;
|
||||
// Языки аудиодорожек в порядке их следования — индекс в списке = порядковый номер 0:a:N.
|
||||
var audioLanguages = new List<string?>();
|
||||
if (root.TryGetProperty("streams", out var streams))
|
||||
{
|
||||
foreach (var stream in streams.EnumerateArray())
|
||||
{
|
||||
if (!stream.TryGetProperty("codec_type", out var typeEl))
|
||||
continue;
|
||||
if (typeEl.GetString() == "video" && !hasVideo)
|
||||
var codecType = typeEl.GetString();
|
||||
if (codecType == "video" && !hasVideo)
|
||||
{
|
||||
hasVideo = true;
|
||||
width = stream.TryGetProperty("width", out var w) ? w.GetInt32() : 0;
|
||||
height = stream.TryGetProperty("height", out var h) ? h.GetInt32() : 0;
|
||||
}
|
||||
else if (codecType == "audio")
|
||||
{
|
||||
var lang =
|
||||
stream.TryGetProperty("tags", out var tags)
|
||||
&& tags.TryGetProperty("language", out var langEl)
|
||||
? langEl.GetString()
|
||||
: null;
|
||||
audioLanguages.Add(lang);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,9 +252,23 @@ public sealed class FfmpegMediaProcessor(
|
||||
if (!hasVideo || width <= 0 || height <= 0)
|
||||
throw new InvalidOperationException("В файле не найдена видеодорожка.");
|
||||
|
||||
return new ProbeInfo(duration, width, height);
|
||||
return new ProbeInfo(duration, width, height, audioLanguages);
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyList<string>> GetPreferredLanguagesAsync(
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
await using var scope = scopeFactory.CreateAsyncScope();
|
||||
var settings = scope.ServiceProvider.GetRequiredService<ISiteSettings>();
|
||||
var configured = await settings.GetPreferredAudioLanguagesAsync(cancellationToken);
|
||||
var raw = string.IsNullOrWhiteSpace(configured) ? _media.PreferredAudioLanguages : configured;
|
||||
return raw.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Select(x => x.ToLowerInvariant())
|
||||
.ToList();
|
||||
}
|
||||
|
||||
|
||||
private static (int Width, int Height) ScaleDown(int width, int height)
|
||||
{
|
||||
if (width <= MaxWidth)
|
||||
@@ -240,5 +289,10 @@ public sealed class FfmpegMediaProcessor(
|
||||
return text.Length <= max ? text : text[^max..];
|
||||
}
|
||||
|
||||
private sealed record ProbeInfo(double Duration, int Width, int Height);
|
||||
private sealed record ProbeInfo(
|
||||
double Duration,
|
||||
int Width,
|
||||
int Height,
|
||||
IReadOnlyList<string?> AudioLanguages
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,4 +36,9 @@ public sealed class MediaOptions
|
||||
/// <summary>Таймаут одного вызова ffmpeg (транскод/рендер заставки), секунды (0 — без таймаута).
|
||||
/// Не даёт зависшему процессу вечно держать слот параллелизма. Поднять для очень длинных файлов.</summary>
|
||||
public int TranscodeTimeoutSeconds { get; init; } = 1800;
|
||||
|
||||
/// <summary>Предпочитаемые языки аудиодорожек по умолчанию, через запятую в порядке приоритета
|
||||
/// (напр. «rus,eng»). Используется, если админ не задал список в настройках сайта. Пусто — без
|
||||
/// предпочтения (дефолтный выбор ffmpeg).</summary>
|
||||
public string PreferredAudioLanguages { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
@@ -14,13 +14,23 @@ public sealed class SiteSettings(IAppDbContext dbContext) : ISiteSettings
|
||||
|
||||
public async Task SetRegistrationEnabledAsync(bool enabled, CancellationToken cancellationToken)
|
||||
{
|
||||
var value = enabled ? "true" : "false";
|
||||
await UpsertAsync(SettingKeys.RegistrationEnabled, enabled ? "true" : "false", cancellationToken);
|
||||
}
|
||||
|
||||
public Task<string> GetPreferredAudioLanguagesAsync(CancellationToken cancellationToken) =>
|
||||
dbContext.GetStringSettingAsync(SettingKeys.PreferredAudioLanguages, "", cancellationToken);
|
||||
|
||||
public Task SetPreferredAudioLanguagesAsync(string value, CancellationToken cancellationToken) =>
|
||||
UpsertAsync(SettingKeys.PreferredAudioLanguages, value, cancellationToken);
|
||||
|
||||
private async Task UpsertAsync(string key, string value, CancellationToken cancellationToken)
|
||||
{
|
||||
var existing = await dbContext.AppSettings.FirstOrDefaultAsync(
|
||||
s => s.Key == SettingKeys.RegistrationEnabled,
|
||||
s => s.Key == key,
|
||||
cancellationToken
|
||||
);
|
||||
if (existing is null)
|
||||
dbContext.AppSettings.Add(AppSetting.Create(SettingKeys.RegistrationEnabled, value));
|
||||
dbContext.AppSettings.Add(AppSetting.Create(key, value));
|
||||
else
|
||||
existing.SetValue(value);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user