88 lines
3.7 KiB
C#
88 lines
3.7 KiB
C#
using Microsoft.Extensions.DependencyInjection;
|
||
using Microsoft.Extensions.Logging;
|
||
using TeleWave.Application.Broadcast.Bumpers;
|
||
using TeleWave.Application.Common.Interfaces;
|
||
using TeleWave.Domain.Media;
|
||
|
||
namespace TeleWave.Infrastructure.Media;
|
||
|
||
/// <summary>Захваченная на рендер заставка: спецификация собирается уже в самой работе.</summary>
|
||
internal sealed record BumperRenderJob(Guid AssetId);
|
||
|
||
/// <summary>
|
||
/// Асинхронно рендерит ТВ-заставки расписания: планировщик лишь создаёт ассет (Source=Generated) в
|
||
/// статусе Pending и кэш-строку <see cref="Domain.Broadcast.BumperAsset"/>, а сам ffmpeg крутится здесь,
|
||
/// вне тика планировщика и его транзакции. Захват работы и устойчивость к рестарту — в
|
||
/// <see cref="MediaClaimingBackgroundService{TJob}"/>. До готовности ассета плейлист отдаёт филлер.
|
||
///
|
||
/// Рендер строго последовательный: ffmpeg заставки короткий, а параллелить его смысла нет —
|
||
/// очередь разбирается быстрее, чем планировщик успевает её пополнять.
|
||
/// </summary>
|
||
internal sealed class BumperRenderBackgroundService(
|
||
IBumperRenderQueue queue,
|
||
IServiceScopeFactory scopeFactory,
|
||
IBumperRenderer renderer,
|
||
ILogger<BumperRenderBackgroundService> logger
|
||
) : MediaClaimingBackgroundService<BumperRenderJob>(scopeFactory, logger)
|
||
{
|
||
protected override bool HandlesGenerated => true;
|
||
|
||
protected override string LoopErrorMessage => "Ошибка цикла рендера заставок";
|
||
|
||
protected override ValueTask WaitForWorkAsync(CancellationToken cancellationToken) =>
|
||
queue.WaitAsync(cancellationToken);
|
||
|
||
protected override BumperRenderJob ToJob(MediaAsset asset) => new(asset.Id);
|
||
|
||
protected override async Task ProcessAsync(
|
||
BumperRenderJob job,
|
||
CancellationToken cancellationToken
|
||
)
|
||
{
|
||
try
|
||
{
|
||
var spec = await WithScopeAsync<BumperSpecLoader, BumperRenderSpec?>(loader =>
|
||
loader.LoadAsync(job.AssetId, cancellationToken)
|
||
);
|
||
if (spec is null)
|
||
{
|
||
await FailAsync(
|
||
job.AssetId,
|
||
"Не удалось восстановить спецификацию заставки",
|
||
cancellationToken
|
||
);
|
||
return;
|
||
}
|
||
|
||
var render = await renderer.RenderAsync(job.AssetId, spec, cancellationToken);
|
||
|
||
await WithAssetAsync(
|
||
job.AssetId,
|
||
asset =>
|
||
asset.MarkReady(
|
||
new MediaReadyInfo(
|
||
render.Duration,
|
||
render.SegmentSeconds,
|
||
render.SegmentCount,
|
||
render.Width,
|
||
render.Height,
|
||
"h264",
|
||
"aac",
|
||
render.RelativePath
|
||
)
|
||
),
|
||
cancellationToken
|
||
);
|
||
}
|
||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||
{
|
||
throw;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
logger.LogError(ex, "Рендер заставки {AssetId} провалился", job.AssetId);
|
||
await FailAsync(job.AssetId, ex.Message, CancellationToken.None);
|
||
}
|
||
}
|
||
}
|